Merge "[SB][RONs] Remove duplicate StatusBarRonChips class." 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/api/StubLibraries.bp b/api/StubLibraries.bp
index b3a674f..d1aa23c 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -466,6 +466,32 @@
 }
 
 java_library {
+    name: "android-non-updatable.stubs.system_server",
+    defaults: ["android-non-updatable_defaults"],
+    static_libs: [
+        "android-non-updatable.stubs.system_server.from-source",
+    ],
+    product_variables: {
+        build_from_text_stub: {
+            static_libs: [
+                "android-non-updatable.stubs.system_server.from-text",
+            ],
+            exclude_static_libs: [
+                "android-non-updatable.stubs.system_server.from-source",
+            ],
+        },
+    },
+}
+
+java_library {
+    name: "android-non-updatable.stubs.exportable.system_server",
+    defaults: ["android-non-updatable_defaults"],
+    static_libs: [
+        "android-non-updatable.stubs.exportable.system_server.from-source",
+    ],
+}
+
+java_library {
     name: "android-non-updatable.stubs.from-source",
     defaults: [
         "android-non-updatable_defaults",
@@ -561,6 +587,30 @@
     },
 }
 
+java_library {
+    name: "android-non-updatable.stubs.system_server.from-source",
+    defaults: [
+        "android-non-updatable_defaults",
+        "android-non-updatable_from_source_defaults",
+    ],
+    srcs: [":services-non-updatable-stubs"],
+    libs: non_updatable_api_deps_on_modules,
+}
+
+java_library {
+    name: "android-non-updatable.stubs.exportable.system_server.from-source",
+    defaults: [
+        "android-non-updatable_defaults",
+        "android-non-updatable_from_source_defaults",
+        "android-non-updatable_exportable_from_source_defaults",
+    ],
+    srcs: [":services-non-updatable-stubs{.exportable}"],
+    libs: non_updatable_api_deps_on_modules,
+    dist: {
+        dir: "apistubs/android/system-server",
+    },
+}
+
 java_defaults {
     name: "android-non-updatable_from_text_defaults",
     defaults: ["android-non-updatable-stubs-libs-defaults"],
@@ -662,6 +712,25 @@
     libs: ["all-modules-system-stubs"],
 }
 
+java_api_library {
+    name: "android-non-updatable.stubs.system_server.from-text",
+    api_surface: "system_server",
+    api_contributions: [
+        "api-stubs-docs-non-updatable.api.contribution",
+        "system-api-stubs-docs-non-updatable.api.contribution",
+        "module-lib-api-stubs-docs-non-updatable.api.contribution",
+        "services-non-updatable-stubs.api.contribution",
+    ],
+    defaults: [
+        "module-classpath-java-defaults",
+        "android-non-updatable_everything_from_text_defaults",
+    ],
+
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.system-server.latest",
+}
+
 java_defaults {
     name: "android_stubs_dists_default",
     dist: {
@@ -813,9 +882,9 @@
     defaults: [
         "android.jar_defaults",
     ],
-    srcs: [":services-non-updatable-stubs"],
     installable: false,
     static_libs: [
+        "android-non-updatable.stubs.system_server",
         "android_module_lib_stubs_current",
     ],
     visibility: ["//frameworks/base/services"],
@@ -827,9 +896,9 @@
         "android.jar_defaults",
         "android_stubs_dists_default",
     ],
-    srcs: [":services-non-updatable-stubs{.exportable}"],
     installable: false,
     static_libs: [
+        "android-non-updatable.stubs.exportable.system_server",
         "android_module_lib_stubs_current_exportable",
     ],
     dist: {
diff --git a/cmds/uiautomator/library/Android.bp b/cmds/uiautomator/library/Android.bp
index 966bf13..5c5b220 100644
--- a/cmds/uiautomator/library/Android.bp
+++ b/cmds/uiautomator/library/Android.bp
@@ -28,9 +28,9 @@
         "testrunner-src/**/*.java",
     ],
     libs: [
-        "android.test.runner",
+        "android.test.runner.stubs.system",
         "junit",
-        "android.test.base",
+        "android.test.base.stubs.system",
         "unsupportedappusage",
     ],
     installable: false,
@@ -56,9 +56,9 @@
         ":uiautomator-stubs",
     ],
     libs: [
-        "android.test.runner",
+        "android.test.runner.stubs",
         "junit",
-        "android.test.base",
+        "android.test.base.stubs",
     ],
     sdk_version: "current",
     installable: false,
diff --git a/core/api/current.txt b/core/api/current.txt
index 5d134c6..542543d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -32613,6 +32613,13 @@
     method public boolean isCharging();
     field public static final String ACTION_CHARGING = "android.os.action.CHARGING";
     field public static final String ACTION_DISCHARGING = "android.os.action.DISCHARGING";
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_CRITICAL = 1; // 0x1
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_FULL = 5; // 0x5
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_HIGH = 4; // 0x4
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_LOW = 2; // 0x2
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_NORMAL = 3; // 0x3
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_UNKNOWN = 0; // 0x0
+    field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_UNSUPPORTED = -1; // 0xffffffff
     field public static final int BATTERY_HEALTH_COLD = 7; // 0x7
     field public static final int BATTERY_HEALTH_DEAD = 4; // 0x4
     field public static final int BATTERY_HEALTH_GOOD = 2; // 0x2
@@ -32637,6 +32644,7 @@
     field public static final int BATTERY_STATUS_NOT_CHARGING = 4; // 0x4
     field public static final int BATTERY_STATUS_UNKNOWN = 1; // 0x1
     field public static final String EXTRA_BATTERY_LOW = "battery_low";
+    field @FlaggedApi("android.os.battery_part_status_api") public static final String EXTRA_CAPACITY_LEVEL = "android.os.extra.CAPACITY_LEVEL";
     field public static final String EXTRA_CHARGING_STATUS = "android.os.extra.CHARGING_STATUS";
     field public static final String EXTRA_CYCLE_COUNT = "android.os.extra.CYCLE_COUNT";
     field public static final String EXTRA_HEALTH = "health";
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/api/test-current.txt b/core/api/test-current.txt
index caf6992..72a68f8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1797,18 +1797,20 @@
 
   public class InputSettings {
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") public static int getAccessibilityBounceKeysThreshold(@NonNull android.content.Context);
-    method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") public static int getAccessibilityRepeatKeysDelay(@NonNull android.content.Context);
-    method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") public static int getAccessibilityRepeatKeysTimeout(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") public static int getAccessibilitySlowKeysThreshold(@NonNull android.content.Context);
+    method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static int getRepeatKeysDelay(@NonNull android.content.Context);
+    method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static int getRepeatKeysTimeout(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") public static boolean isAccessibilityMouseKeysEnabled(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") public static boolean isAccessibilityStickyKeysEnabled(@NonNull android.content.Context);
+    method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static boolean isRepeatKeysEnabled(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityBounceKeysThreshold(@NonNull android.content.Context, int);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityMouseKeysEnabled(@NonNull android.content.Context, boolean);
-    method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityRepeatKeysDelay(@NonNull android.content.Context, int);
-    method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityRepeatKeysTimeout(@NonNull android.content.Context, int);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilitySlowKeysThreshold(@NonNull android.content.Context, int);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityStickyKeysEnabled(@NonNull android.content.Context, boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float);
+    method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysDelay(@NonNull android.content.Context, int);
+    method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysEnabled(@NonNull android.content.Context, boolean);
+    method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysTimeout(@NonNull android.content.Context, int);
     field public static final int DEFAULT_POINTER_SPEED = 0; // 0x0
   }
 
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/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index c27141a..0d981ea 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -26,6 +26,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
 import android.os.IBinder;
@@ -60,29 +61,53 @@
     @NonNull
     public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
 
-    private final Binder mBinder =
-            new IAppFunctionService.Stub() {
-                @Override
-                public void executeAppFunction(
-                        @NonNull ExecuteAppFunctionRequest request,
-                        @NonNull IExecuteAppFunctionCallback callback) {
-                    if (AppFunctionService.this.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
-                            == PERMISSION_DENIED) {
-                        throw new SecurityException("Can only be called by the system server.");
-                    }
-                    SafeOneTimeExecuteAppFunctionCallback safeCallback =
-                            new SafeOneTimeExecuteAppFunctionCallback(callback);
-                    try {
-                        AppFunctionService.this.onExecuteFunction(request, safeCallback::onResult);
-                    } catch (Exception ex) {
-                        // Apps should handle exceptions. But if they don't, report the error on
-                        // behalf of them.
-                        safeCallback.onResult(
-                                ExecuteAppFunctionResponse.newFailure(
-                                        getResultCode(ex), ex.getMessage(), /* extras= */ null));
-                    }
+    /**
+     * Functional interface to represent the execution logic of an app function.
+     *
+     * @hide
+     */
+    @FunctionalInterface
+    public interface OnExecuteFunction {
+        /**
+         * Performs the semantic of executing the function specified by the provided request and
+         * return the response through the provided callback.
+         */
+        void perform(
+                @NonNull ExecuteAppFunctionRequest request,
+                @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+    }
+
+    /** @hide */
+    @NonNull
+    public static Binder createBinder(
+            @NonNull Context context, @NonNull OnExecuteFunction onExecuteFunction) {
+        return new IAppFunctionService.Stub() {
+            @Override
+            public void executeAppFunction(
+                    @NonNull ExecuteAppFunctionRequest request,
+                    @NonNull IExecuteAppFunctionCallback callback) {
+                if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
+                        == PERMISSION_DENIED) {
+                    throw new SecurityException("Can only be called by the system server.");
                 }
-            };
+                SafeOneTimeExecuteAppFunctionCallback safeCallback =
+                        new SafeOneTimeExecuteAppFunctionCallback(callback);
+                try {
+                    onExecuteFunction.perform(request, safeCallback::onResult);
+                } catch (Exception ex) {
+                    // Apps should handle exceptions. But if they don't, report the error on
+                    // behalf of them.
+                    safeCallback.onResult(
+                            ExecuteAppFunctionResponse.newFailure(
+                                    getResultCode(ex), ex.getMessage(), /* extras= */ null));
+                }
+            }
+        };
+    }
+
+    private final Binder mBinder = createBinder(
+            AppFunctionService.this,
+            AppFunctionService.this::onExecuteFunction);
 
     @NonNull
     @Override
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/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 9891e89..9b06adf 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -236,4 +236,12 @@
   namespace: "systemui"
   description: "Guards new android.app.richongoingnotification api"
   bug: "337261753"
+}
+
+flag {
+  name: "ui_rich_ongoing"
+  is_exported: true
+  namespace: "systemui"
+  description: "Guards new android.app.richongoingnotification promotion and new uis"
+  bug: "337261753"
 }
\ No newline at end of file
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/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index ffadd1e..2cdae21 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -206,6 +206,17 @@
             ]
         },
         {
+            "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+            "options":[
+               {
+                   "exclude-annotation":"androidx.test.filters.FlakyTest"
+               },
+               {
+                   "exclude-annotation":"org.junit.Ignore"
+               }
+            ]
+        },
+        {
             "name": "CtsPackageInstallerCUJInstallationTestCases",
             "options":[
                {
@@ -217,6 +228,17 @@
             ]
         },
         {
+            "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+            "options":[
+               {
+                   "exclude-annotation":"androidx.test.filters.FlakyTest"
+               },
+               {
+                   "exclude-annotation":"org.junit.Ignore"
+               }
+            ]
+        },
+        {
             "name": "CtsPackageInstallerCUJUninstallationTestCases",
             "options":[
                {
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/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 8592ded..177ee6f 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -20,15 +20,15 @@
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS;
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG;
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG;
-import static com.android.hardware.input.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
 import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
-import static com.android.hardware.input.Flags.keyboardRepeatKeys;
 import static com.android.hardware.input.Flags.touchpadTapDragging;
 import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
+import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
+import static com.android.input.flags.Flags.keyboardRepeatKeys;
 
 import android.Manifest;
 import android.annotation.FlaggedApi;
@@ -800,7 +800,7 @@
      *
      * <p>
      * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
-     * key on the physical keyboard is held down. This accessibility feature allows the user
+     * key on the physical keyboard is held down. This feature allows the user
      * to configure the timeout before the key repeats begin as well as the delay
      * between successive key repeats.
      * </p>
@@ -812,7 +812,31 @@
     }
 
     /**
-     * Get Accessibility repeat keys timeout duration in milliseconds.
+     * Whether "Repeat keys" feature is enabled.
+     * Repeat keys is ON by default.
+     * The repeat keys timeout and delay would have the default values in the default ON case.
+     *
+     * <p>
+     * 'Repeat keys’ is a feature which allows users to generate key repeats when a particular
+     * key on the physical keyboard is held down. This feature allows the user
+     * to configure the timeout before the key repeats begin as well as the delay
+     * between successive key repeats.
+     * </p>
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
+    public static boolean isRepeatKeysEnabled(@NonNull Context context) {
+        if (!isRepeatKeysFeatureFlagEnabled()) {
+            return true;
+        }
+        return Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.KEY_REPEAT_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
+    }
+
+    /**
+     * Get repeat keys timeout duration in milliseconds.
      * The default key repeat timeout is {@link ViewConfiguration#DEFAULT_KEY_REPEAT_TIMEOUT_MS}.
      *
      * @param context The application context
@@ -823,7 +847,7 @@
      *
      * <p>
      * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
-     * key on the physical keyboard is held down. This accessibility feature allows the user
+     * key on the physical keyboard is held down. This feature allows the user
      * to configure the timeout before the key repeats begin as well as the delay
      * between successive key repeats.
      * </p>
@@ -832,14 +856,17 @@
      */
     @TestApi
     @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
-    public static int getAccessibilityRepeatKeysTimeout(@NonNull Context context) {
+    public static int getRepeatKeysTimeout(@NonNull Context context) {
+        if (!isRepeatKeysFeatureFlagEnabled()) {
+            return ViewConfiguration.getKeyRepeatTimeout();
+        }
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 Settings.Secure.KEY_REPEAT_TIMEOUT_MS, ViewConfiguration.getKeyRepeatTimeout(),
                 UserHandle.USER_CURRENT);
     }
 
     /**
-     * Get Accessibility repeat keys delay rate in milliseconds.
+     * Get repeat keys delay rate in milliseconds.
      * The default key repeat delay is {@link ViewConfiguration#DEFAULT_KEY_REPEAT_DELAY_MS}.
      *
      * @param context The application context
@@ -850,7 +877,7 @@
      *
      * <p>
      * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
-     * key on the physical keyboard is held down. This accessibility feature allows the user
+     * key on the physical keyboard is held down. This feature allows the user
      * to configure the timeout before the key repeats begin as well as the delay
      * between successive key repeats.
      * </p>
@@ -859,14 +886,41 @@
      */
     @TestApi
     @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
-    public static int getAccessibilityRepeatKeysDelay(@NonNull Context context) {
+    public static int getRepeatKeysDelay(@NonNull Context context) {
+        if (!isRepeatKeysFeatureFlagEnabled()) {
+            return ViewConfiguration.getKeyRepeatDelay();
+        }
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 Settings.Secure.KEY_REPEAT_DELAY_MS, ViewConfiguration.getKeyRepeatDelay(),
                 UserHandle.USER_CURRENT);
     }
 
     /**
-     * Set Accessibility repeat keys timeout duration in milliseconds.
+     * Set repeat keys feature enabled/disabled.
+     *
+     * <p>
+     * 'Repeat keys’ is a feature which allows users to generate key repeats when a particular
+     * key on the physical keyboard is held down. This feature allows the user
+     * to configure the timeout before the key repeats begin as well as the delay
+     * between successive key repeats.
+     * </p>
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setRepeatKeysEnabled(@NonNull Context context,
+            boolean enabled) {
+        if (!isRepeatKeysFeatureFlagEnabled()) {
+            return;
+        }
+        Settings.Secure.putIntForUser(context.getContentResolver(),
+                Settings.Secure.KEY_REPEAT_ENABLED, enabled ? 1 : 0, UserHandle.USER_CURRENT);
+    }
+
+    /**
+     * Set repeat keys timeout duration in milliseconds.
      *
      * @param timeoutTimeMillis time duration for which a key should be pressed after which the
      *                          pressed key will be repeated. The timeout must be between
@@ -875,7 +929,7 @@
      *
      *  <p>
      * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
-     * key on the physical keyboard is held down. This accessibility feature allows the user
+     * key on the physical keyboard is held down. This feature allows the user
      * to configure the timeout before the key repeats begin as well as the delay
      *  between successive key repeats.
      * </p>
@@ -885,8 +939,12 @@
     @TestApi
     @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
     @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
-    public static void setAccessibilityRepeatKeysTimeout(@NonNull Context context,
+    public static void setRepeatKeysTimeout(@NonNull Context context,
             int timeoutTimeMillis) {
+        if (!isRepeatKeysFeatureFlagEnabled()
+                && !isRepeatKeysEnabled(context)) {
+            return;
+        }
         if (timeoutTimeMillis < MIN_KEY_REPEAT_TIMEOUT_MILLIS
                 || timeoutTimeMillis > MAX_KEY_REPEAT_TIMEOUT_MILLIS) {
             throw new IllegalArgumentException(
@@ -900,7 +958,7 @@
     }
 
     /**
-     * Set Accessibility repeat key delay duration in milliseconds.
+     * Set repeat key delay duration in milliseconds.
      *
      * @param delayTimeMillis Time duration between successive key repeats when a key is
      *                        pressed down. The delay duration must be between
@@ -908,7 +966,7 @@
      *                        {@link #MAX_KEY_REPEAT_DELAY_MILLIS}
      * <p>
      * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
-     * key on the physical keyboard is held down. This accessibility feature allows the user
+     * key on the physical keyboard is held down. This feature allows the user
      * to configure the timeout before the key repeats begin as well as the delay
      * between successive key repeats.
      * </p>
@@ -918,8 +976,12 @@
     @TestApi
     @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
     @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
-    public static void setAccessibilityRepeatKeysDelay(@NonNull Context context,
+    public static void setRepeatKeysDelay(@NonNull Context context,
             int delayTimeMillis) {
+        if (!isRepeatKeysFeatureFlagEnabled()
+                && !isRepeatKeysEnabled(context)) {
+            return;
+        }
         if (delayTimeMillis < MIN_KEY_REPEAT_DELAY_MILLIS
                 || delayTimeMillis > MAX_KEY_REPEAT_DELAY_MILLIS) {
             throw new IllegalArgumentException(
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index 6f11d3a..af93c96 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -35,7 +35,6 @@
 import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.vcn.util.PersistableBundleUtils;
 
@@ -434,7 +433,14 @@
     @NonNull
     public int[] getExposedCapabilities() {
         // Sorted set guarantees ordering
-        return ArrayUtils.convertToIntArray(new ArrayList<>(mExposedCapabilities));
+        final int[] caps = new int[mExposedCapabilities.size()];
+
+        int i = 0;
+        for (int c : mExposedCapabilities) {
+            caps[i++] = c;
+        }
+
+        return caps;
     }
 
     /**
diff --git a/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java b/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java
index a975637..e1d1b3c6 100644
--- a/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java
+++ b/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java
@@ -24,7 +24,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
-import com.android.internal.util.ArrayUtils;
 
 import java.util.Arrays;
 import java.util.Objects;
@@ -114,8 +113,13 @@
     @Override
     public boolean canBeSatisfiedBy(NetworkSpecifier other) {
         if (other instanceof TelephonyNetworkSpecifier) {
-            return ArrayUtils.contains(
-                    mSubIds, ((TelephonyNetworkSpecifier) other).getSubscriptionId());
+            final int targetSubId = ((TelephonyNetworkSpecifier) other).getSubscriptionId();
+            for (int subId : mSubIds) {
+                if (targetSubId == subId) {
+                    return true;
+                }
+            }
+            return false;
         }
         // TODO(b/180140053): Allow matching against WifiNetworkAgentSpecifier
 
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index f3efd89..8b267bf 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -141,6 +141,7 @@
     /**
      * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
      * integer containing the charge counter present in the battery.
+     * It shows the available battery power in µAh
      * {@hide}
      */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -166,6 +167,76 @@
     public static final String EXTRA_CHARGING_STATUS = "android.os.extra.CHARGING_STATUS";
 
     /**
+     * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
+     * Int value representing the battery's capacity level. These constants are key indicators of
+     * battery status and system capabilities, guiding power management decisions for both the
+     * system and apps:
+     * {@link #BATTERY_CAPACITY_LEVEL_UNSUPPORTED}: Feature not supported on this device.
+     * {@link #BATTERY_CAPACITY_LEVEL_UNKNOWN}: Battery status is unavailable or uninitialized.
+     * {@link #BATTERY_CAPACITY_LEVEL_CRITICAL}: Battery is critically low and the Android
+     * framework has been notified to schedule a shutdown by this value
+     * {@link #BATTERY_CAPACITY_LEVEL_LOW}: Android framework must limit background jobs to
+     * avoid impacting charging speed
+     * {@link #BATTERY_CAPACITY_LEVEL_NORMAL}: Battery level and charging rates are normal,
+     * battery temperature is within normal range and adapter power is enough to charge the
+     * battery at an acceptable rate. Android framework can run light background tasks without
+     * affecting charging performance severely.
+     * {@link #BATTERY_CAPACITY_LEVEL_HIGH}: Battery level is high, battery temperature is
+     * within normal range and adapter power is enough to charge the battery at an acceptable
+     * rate while running background loads. Android framework can run background tasks without
+     * affecting charging or battery performance.
+     * {@link #BATTERY_CAPACITY_LEVEL_FULL}: The battery is full, battery temperature is
+     * within normal range and adapter power is enough to sustain running background loads.
+     * Android framework can run background tasks without affecting the battery level or
+     * battery performance.
+     */
+
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final String EXTRA_CAPACITY_LEVEL = "android.os.extra.CAPACITY_LEVEL";
+
+    /**
+     * Battery capacity level is unsupported. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_UNSUPPORTED = -1;
+
+    /**
+     * Battery capacity level is unknown. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_UNKNOWN = 0;
+
+    /**
+     * Battery capacity level is critical. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_CRITICAL = 1;
+
+    /**
+     * Battery capacity level is low. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_LOW = 2;
+
+    /**
+     * Battery capacity level is normal. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_NORMAL = 3;
+
+    /**
+     * Battery capacity level is high. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_HIGH = 4;
+
+    /**
+     * Battery capacity level is full. @see EXTRA_CAPACITY_LEVEL
+     */
+    @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+    public static final int BATTERY_CAPACITY_LEVEL_FULL = 5;
+
+    /**
      * Extra for {@link android.content.Intent#ACTION_BATTERY_LEVEL_CHANGED}:
      * Contains list of Bundles representing battery events
      * @hide
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/provider/Settings.java b/core/java/android/provider/Settings.java
index 0ada993..b8a8be1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9158,15 +9158,27 @@
         public static final String MULTI_PRESS_TIMEOUT = "multi_press_timeout";
 
         /**
+         * Whether to enable key repeats for Physical Keyboard.
+         *
+         * If set to false, continuous key presses on
+         * physical keyboard will not cause the pressed key to repeated.
+         * @hide
+         */
+        @Readable
+        public static final String KEY_REPEAT_ENABLED = "key_repeat_enabled";
+
+        /**
          * The duration before a key repeat begins in milliseconds.
          * @hide
          */
+        @Readable
         public static final String KEY_REPEAT_TIMEOUT_MS = "key_repeat_timeout";
 
         /**
          * The duration between successive key repeats in milliseconds.
          * @hide
          */
+        @Readable
         public static final String KEY_REPEAT_DELAY_MS = "key_repeat_delay";
 
         /**
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java
index c2ad508..ca88764 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/core/java/android/text/ClientFlags.java
@@ -16,21 +16,14 @@
 
 package android.text;
 
-import com.android.text.flags.Flags;
-
 /**
  * An aconfig feature flags that can be accessible from application process without
  * ContentProvider IPCs.
  *
  * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
  *
+ * TODO(nona): Remove this class.
  * @hide
  */
 public class ClientFlags {
-    /**
-     * @see Flags#fixMisalignedContextMenu()
-     */
-    public static boolean fixMisalignedContextMenu() {
-        return TextFlags.isFeatureEnabled(Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU);
-    }
 }
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
index 076721f..f69a333 100644
--- a/core/java/android/text/TextFlags.java
+++ b/core/java/android/text/TextFlags.java
@@ -19,11 +19,10 @@
 import android.annotation.NonNull;
 import android.app.AppGlobals;
 
-import com.android.text.flags.Flags;
-
 /**
  * Flags in the "text" namespace.
  *
+ * TODO(nona): Remove this class.
  * @hide
  */
 public final class TextFlags {
@@ -55,7 +54,6 @@
      * List of text flags to be transferred to the application process.
      */
     public static final String[] TEXT_ACONFIGS_FLAGS = {
-            Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU,
     };
 
     /**
@@ -64,7 +62,6 @@
      * The order must be the same to the TEXT_ACONFIG_FLAGS.
      */
     public static final boolean[] TEXT_ACONFIG_DEFAULT_VALUE = {
-            Flags.fixMisalignedContextMenu(),
     };
 
     /**
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3846972..c83285a 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -84,16 +84,6 @@
 }
 
 flag {
-  name: "fix_misaligned_context_menu"
-  namespace: "text"
-  description: "Fix the context menu misalignment and incosistent icon size."
-  bug: "332542108"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "missing_getter_apis"
   namespace: "text"
   description: "Fix the lint warning about missing getters."
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/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 13648de..9ae3fc1 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -122,3 +122,10 @@
     description: "Requires apps to opt-in to overlay pass through touches and provide APIs to opt-in"
     bug: "358129114"
 }
+
+flag {
+    namespace: "windowing_sdk"
+    name: "wlinfo_oncreate"
+    description: "Makes WindowLayoutInfo accessible without racing in the Activity#onCreate()"
+    bug: "337820752"
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index e440dc9..fbc058c 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -210,6 +210,8 @@
                                 DataSourceParams
                                         .PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
                         .build();
+        // NOTE: Registering that datasource is an async operation, so there may be no data traced
+        // for some messages logged right after the construction of this class.
         mDataSource.register(params);
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
         this.mViewerConfigReader = viewerConfigReader;
@@ -223,17 +225,17 @@
                     "ServiceManager returned a null ProtoLog Configuration Service");
 
             try {
-                var args = new ProtoLogConfigurationService.RegisterClientArgs();
+                var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs();
 
                 if (viewerConfigFilePath != null) {
                     args.setViewerConfigFile(viewerConfigFilePath);
                 }
 
                 final var groupArgs = Stream.of(groups)
-                        .map(group -> new ProtoLogConfigurationService.RegisterClientArgs
+                        .map(group -> new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(group.name(), group.isLogToLogcat()))
-                        .toArray(
-                                ProtoLogConfigurationService.RegisterClientArgs.GroupConfig[]::new);
+                        .toArray(ProtoLogConfigurationServiceImpl
+                                .RegisterClientArgs.GroupConfig[]::new);
                 args.setGroups(groupArgs);
 
                 mProtoLogConfigurationService.registerClient(this, args);
diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java
index 7031d69..d65aaae 100644
--- a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java
+++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java
@@ -16,434 +16,32 @@
 
 package com.android.internal.protolog;
 
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
-
 import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SystemService;
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.ResultReceiver;
-import android.os.ShellCallback;
-import android.tracing.perfetto.DataSourceParams;
-import android.tracing.perfetto.InitArguments;
-import android.tracing.perfetto.Producer;
-import android.util.Log;
-import android.util.proto.ProtoInputStream;
-import android.util.proto.ProtoOutputStream;
 
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-/**
- * The ProtoLog service is responsible for orchestrating centralized actions of the protolog tracing
- * system. Currently this service has the following roles:
- * - Handle shell commands to toggle logging ProtoLog messages for specified groups to logcat.
- * - Handle viewer config dumping (the mapping from message hash to message string) for all protolog
- *   clients. This is for two reasons: firstly, because client processes might be frozen so might
- *   not response to the request to dump their viewer config when the trace is stopped; secondly,
- *   multiple processes might be running the same code with the same viewer config, this centralized
- *   service ensures we don't dump the same viewer config multiple times across processes.
- * <p>
- * {@link com.android.internal.protolog.IProtoLogClient ProtoLog clients} register themselves to
- * this service on initialization.
- * <p>
- * This service is intended to run on the system server, such that it never gets frozen.
- */
-@SystemService(Context.PROTOLOG_CONFIGURATION_SERVICE)
-public final class ProtoLogConfigurationService extends IProtoLogConfigurationService.Stub {
-    private static final String LOG_TAG = "ProtoLogConfigurationService";
-
-    private final ProtoLogDataSource mDataSource;
-
-    /**
-     * Keeps track of how many of each viewer config file is currently registered.
-     * Use to keep track of which viewer config files are actively being used in tracing and might
-     * need to be dumped on flush.
-     */
-    private final Map<String, Integer> mConfigFileCounts = new HashMap<>();
-    /**
-     * Keeps track of the viewer config file of each client if available.
-     */
-    private final Map<IProtoLogClient, String> mClientConfigFiles = new HashMap<>();
-
-    /**
-     * Keeps track of all the protolog groups that have been registered by clients and are still
-     * being actively traced.
-     */
-    private final Set<String> mRegisteredGroups = new HashSet<>();
-    /**
-     * Keeps track of all the clients that are actively tracing a given protolog group.
-     */
-    private final Map<String, Set<IProtoLogClient>> mGroupToClients = new HashMap<>();
-
-    /**
-     * Keeps track of whether or not a given group should be logged to logcat.
-     * True when logging to logcat, false otherwise.
-     */
-    private final Map<String, Boolean> mLogGroupToLogcatStatus = new TreeMap<>();
-
-    /**
-     * Keeps track of all the tracing instance ids that are actively running for ProtoLog.
-     */
-    private final Set<Integer> mRunningInstances = new HashSet<>();
-
-    private final ViewerConfigFileTracer mViewerConfigFileTracer;
-
-    public ProtoLogConfigurationService() {
-        this(ProtoLogDataSource::new, ProtoLogConfigurationService::dumpTransitionTraceConfig);
-    }
-
-    @VisibleForTesting
-    public ProtoLogConfigurationService(@NonNull ProtoLogDataSourceBuilder dataSourceBuilder) {
-        this(dataSourceBuilder, ProtoLogConfigurationService::dumpTransitionTraceConfig);
-    }
-
-    @VisibleForTesting
-    public ProtoLogConfigurationService(@NonNull ViewerConfigFileTracer tracer) {
-        this(ProtoLogDataSource::new, tracer);
-    }
-
-    @VisibleForTesting
-    public ProtoLogConfigurationService(
-            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
-            @NonNull ViewerConfigFileTracer tracer) {
-        mDataSource = dataSourceBuilder.build(
-            this::onTracingInstanceStart,
-            this::onTracingInstanceFlush,
-            this::onTracingInstanceStop
-        );
-
-        // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be
-        // receive the lifecycle callbacks of the datasource and write the viewer configs if and
-        // when required to the datasource.
-        Producer.init(InitArguments.DEFAULTS);
-        final var params = new DataSourceParams.Builder()
-                .setBufferExhaustedPolicy(DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
-                .build();
-        mDataSource.register(params);
-
-        mViewerConfigFileTracer = tracer;
-    }
-
-    public static class RegisterClientArgs extends IRegisterClientArgs.Stub {
-        /**
-         * The viewer config file to be registered for this client ProtoLog process.
-         */
-        @Nullable
-        private String mViewerConfigFile = null;
-        /**
-         * The list of all groups that this client protolog process supports and might trace.
-         */
-        @NonNull
-        private String[] mGroups = new String[0];
-        /**
-         * The default logcat status of the ProtoLog client. True is logging to logcat, false
-         * otherwise. The indices should match the indices in {@link mGroups}.
-         */
-        @NonNull
-        private boolean[] mLogcatStatus = new boolean[0];
-
-        public record GroupConfig(@NonNull String group, boolean logToLogcat) {}
-
-        /**
-         * Specify groups to register with this client that will be used for protologging in this
-         * process.
-         * @param groups to register with this client.
-         * @return self
-         */
-        public RegisterClientArgs setGroups(GroupConfig... groups) {
-            mGroups = new String[groups.length];
-            mLogcatStatus = new boolean[groups.length];
-
-            for (int i = 0; i < groups.length; i++) {
-                mGroups[i] = groups[i].group;
-                mLogcatStatus[i] = groups[i].logToLogcat;
-            }
-
-            return this;
-        }
-
-        /**
-         * Set the viewer config file that the logs in this process are using.
-         * @param viewerConfigFile The file path of the viewer config.
-         * @return self
-         */
-        public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) {
-            mViewerConfigFile = viewerConfigFile;
-
-            return this;
-        }
-
-        @Override
-        @NonNull
-        public String[] getGroups() {
-            return mGroups;
-        }
-
-        @Override
-        @NonNull
-        public boolean[] getGroupsDefaultLogcatStatus() {
-            return mLogcatStatus;
-        }
-
-        @Nullable
-        @Override
-        public String getViewerConfigFile() {
-            return mViewerConfigFile;
-        }
-    }
-
-    @FunctionalInterface
-    public interface ViewerConfigFileTracer {
-        /**
-         * Write the viewer config data to the trace buffer.
-         *
-         * @param dataSource The target datasource to write the viewer config to.
-         * @param viewerConfigFilePath The path of the viewer config file which contains the data we
-         *                             want to write to the trace buffer.
-         * @throws FileNotFoundException if the viewerConfigFilePath is invalid.
-         */
-        void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath);
-    }
-
-    @Override
-    public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args)
-            throws RemoteException {
-        client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0);
-
-        final String viewerConfigFile = args.getViewerConfigFile();
-        if (viewerConfigFile != null) {
-            registerViewerConfigFile(client, viewerConfigFile);
-        }
-
-        registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
-    }
-
-    @Override
-    public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
-            @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
-            @NonNull ResultReceiver resultReceiver) throws RemoteException {
-        new ProtoLogCommandHandler(this)
-                .exec(this, in, out, err, args, callback, resultReceiver);
-    }
-
+public interface ProtoLogConfigurationService extends IProtoLogConfigurationService {
     /**
      * Get the list of groups clients have registered to the protolog service.
      * @return The list of ProtoLog groups registered with this service.
      */
     @NonNull
-    public String[] getGroups() {
-        return mRegisteredGroups.toArray(new String[0]);
-    }
-
-    /**
-     * Enable logging target groups to logcat.
-     * @param groups we want to enable logging them to logcat for.
-     */
-    public void enableProtoLogToLogcat(String... groups) {
-        toggleProtoLogToLogcat(true, groups);
-    }
-
-    /**
-     * Disable logging target groups to logcat.
-     * @param groups we want to disable from being logged to logcat.
-     */
-    public void disableProtoLogToLogcat(String... groups) {
-        toggleProtoLogToLogcat(false, groups);
-    }
+    String[] getGroups();
 
     /**
      * Check if a group is logging to logcat
      * @param group The group we want to check for
      * @return True iff we are logging this group to logcat.
      */
-    public boolean isLoggingToLogcat(@NonNull String group) {
-        final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group);
+    boolean isLoggingToLogcat(@NonNull String group);
 
-        if (isLoggingToLogcat == null) {
-            throw new RuntimeException(
-                    "Trying to get logcat logging status of non-registered group " + group);
-        }
+    /**
+     * Enable logging target groups to logcat.
+     * @param groups we want to enable logging them to logcat for.
+     */
+    void enableProtoLogToLogcat(@NonNull String... groups);
 
-        return isLoggingToLogcat;
-    }
-
-    private void registerViewerConfigFile(
-            @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) {
-        final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0);
-        mConfigFileCounts.put(viewerConfigFile, count + 1);
-        mClientConfigFiles.put(client, viewerConfigFile);
-    }
-
-    private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups,
-            @NonNull boolean[] logcatStatuses) throws RemoteException {
-        if (groups.length != logcatStatuses.length) {
-            throw new RuntimeException(
-                    "Expected groups and logcatStatuses to have the same length, "
-                        + "but groups has length " + groups.length
-                        + " and logcatStatuses has length " + logcatStatuses.length);
-        }
-
-        for (int i = 0; i < groups.length; i++) {
-            String group = groups[i];
-            boolean logcatStatus = logcatStatuses[i];
-
-            mRegisteredGroups.add(group);
-
-            mGroupToClients.putIfAbsent(group, new HashSet<>());
-            mGroupToClients.get(group).add(client);
-
-            if (!mLogGroupToLogcatStatus.containsKey(group)) {
-                mLogGroupToLogcatStatus.put(group, logcatStatus);
-            }
-
-            boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group);
-            if (requestedLogToLogcat != logcatStatus) {
-                client.toggleLogcat(requestedLogToLogcat, new String[] { group });
-            }
-        }
-    }
-
-    private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) {
-        final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>();
-
-        for (String group : groups) {
-            final var clients = mGroupToClients.get(group);
-
-            if (clients == null) {
-                // No clients associated to this group
-                Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group
-                        + " with no registered clients.");
-                continue;
-            }
-
-            for (IProtoLogClient client : clients) {
-                clientToGroups.putIfAbsent(client, new HashSet<>());
-                clientToGroups.get(client).add(group);
-            }
-        }
-
-        for (IProtoLogClient client : clientToGroups.keySet()) {
-            try {
-                client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0]));
-            } catch (RemoteException e) {
-                throw new RuntimeException(
-                        "Failed to toggle logcat status for groups on client", e);
-            }
-        }
-
-        for (String group : groups) {
-            mLogGroupToLogcatStatus.put(group, enabled);
-        }
-    }
-
-    private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
-        mRunningInstances.add(instanceIdx);
-    }
-
-    private void onTracingInstanceFlush() {
-        for (String fileName : mConfigFileCounts.keySet()) {
-            mViewerConfigFileTracer.trace(mDataSource, fileName);
-        }
-    }
-
-    private void onTracingInstanceStop(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
-        mRunningInstances.remove(instanceIdx);
-    }
-
-    private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource,
-            @NonNull String viewerConfigFilePath) {
-        Utils.dumpViewerConfig(dataSource, () -> {
-            try {
-                return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
-            } catch (FileNotFoundException e) {
-                throw new RuntimeException(
-                        "Failed to load viewer config file " + viewerConfigFilePath, e);
-            }
-        });
-    }
-
-    private void onClientBinderDeath(@NonNull IProtoLogClient client) {
-        // Dump the tracing config now if no other client is going to dump the same config file.
-        String configFile = mClientConfigFiles.get(client);
-        if (configFile != null) {
-            final var newCount = mConfigFileCounts.get(configFile) - 1;
-            mConfigFileCounts.put(configFile, newCount);
-            boolean lastProcessWithViewerConfig = newCount == 0;
-            if (lastProcessWithViewerConfig) {
-                mViewerConfigFileTracer.trace(mDataSource, configFile);
-            }
-        }
-    }
-
-    private static void writeViewerConfigGroup(
-            @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
-        final long inGroupToken = pis.start(GROUPS);
-        final long outGroupToken = os.start(GROUPS);
-
-        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            switch (pis.getFieldNumber()) {
-                case (int) ID -> {
-                    int id = pis.readInt(ID);
-                    os.write(ID, id);
-                }
-                case (int) NAME -> {
-                    String name = pis.readString(NAME);
-                    os.write(NAME, name);
-                }
-                case (int) TAG -> {
-                    String tag = pis.readString(TAG);
-                    os.write(TAG, tag);
-                }
-                default ->
-                    throw new RuntimeException(
-                            "Unexpected field id " + pis.getFieldNumber());
-            }
-        }
-
-        pis.end(inGroupToken);
-        os.end(outGroupToken);
-    }
-
-    private static void writeViewerConfigMessage(
-            @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
-        final long inMessageToken = pis.start(MESSAGES);
-        final long outMessagesToken = os.start(MESSAGES);
-
-        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            switch (pis.getFieldNumber()) {
-                case (int) MESSAGE_ID -> os.write(MESSAGE_ID,
-                        pis.readLong(MESSAGE_ID));
-                case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE));
-                case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL));
-                case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID));
-                case (int) LOCATION -> os.write(LOCATION, pis.readString(LOCATION));
-                default ->
-                    throw new RuntimeException(
-                            "Unexpected field id " + pis.getFieldNumber());
-            }
-        }
-
-        pis.end(inMessageToken);
-        os.end(outMessagesToken);
-    }
+    /**
+     * Disable logging target groups to logcat.
+     * @param groups we want to disable from being logged to logcat.
+     */
+    void disableProtoLogToLogcat(@NonNull String... groups);
 }
diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
new file mode 100644
index 0000000..e382ac1
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
@@ -0,0 +1,454 @@
+/*
+ * 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.internal.protolog;
+
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * The ProtoLog service is responsible for orchestrating centralized actions of the protolog tracing
+ * system. Currently this service has the following roles:
+ * - Handle shell commands to toggle logging ProtoLog messages for specified groups to logcat.
+ * - Handle viewer config dumping (the mapping from message hash to message string) for all protolog
+ *   clients. This is for two reasons: firstly, because client processes might be frozen so might
+ *   not response to the request to dump their viewer config when the trace is stopped; secondly,
+ *   multiple processes might be running the same code with the same viewer config, this centralized
+ *   service ensures we don't dump the same viewer config multiple times across processes.
+ * <p>
+ * {@link com.android.internal.protolog.IProtoLogClient ProtoLog clients} register themselves to
+ * this service on initialization.
+ * <p>
+ * This service is intended to run on the system server, such that it never gets frozen.
+ */
+@SystemService(Context.PROTOLOG_CONFIGURATION_SERVICE)
+public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationService.Stub
+        implements ProtoLogConfigurationService {
+    private static final String LOG_TAG = "ProtoLogConfigurationService";
+
+    private final ProtoLogDataSource mDataSource;
+
+    /**
+     * Keeps track of how many of each viewer config file is currently registered.
+     * Use to keep track of which viewer config files are actively being used in tracing and might
+     * need to be dumped on flush.
+     */
+    private final Map<String, Integer> mConfigFileCounts = new HashMap<>();
+    /**
+     * Keeps track of the viewer config file of each client if available.
+     */
+    private final Map<IProtoLogClient, String> mClientConfigFiles = new HashMap<>();
+
+    /**
+     * Keeps track of all the protolog groups that have been registered by clients and are still
+     * being actively traced.
+     */
+    private final Set<String> mRegisteredGroups = new HashSet<>();
+    /**
+     * Keeps track of all the clients that are actively tracing a given protolog group.
+     */
+    private final Map<String, Set<IProtoLogClient>> mGroupToClients = new HashMap<>();
+
+    /**
+     * Keeps track of whether or not a given group should be logged to logcat.
+     * True when logging to logcat, false otherwise.
+     */
+    private final Map<String, Boolean> mLogGroupToLogcatStatus = new TreeMap<>();
+
+    /**
+     * Keeps track of all the tracing instance ids that are actively running for ProtoLog.
+     */
+    private final Set<Integer> mRunningInstances = new HashSet<>();
+
+    private final ViewerConfigFileTracer mViewerConfigFileTracer;
+
+    public ProtoLogConfigurationServiceImpl() {
+        this(ProtoLogDataSource::new, ProtoLogConfigurationServiceImpl::dumpTransitionTraceConfig);
+    }
+
+    @VisibleForTesting
+    public ProtoLogConfigurationServiceImpl(@NonNull ProtoLogDataSourceBuilder dataSourceBuilder) {
+        this(dataSourceBuilder, ProtoLogConfigurationServiceImpl::dumpTransitionTraceConfig);
+    }
+
+    @VisibleForTesting
+    public ProtoLogConfigurationServiceImpl(@NonNull ViewerConfigFileTracer tracer) {
+        this(ProtoLogDataSource::new, tracer);
+    }
+
+    @VisibleForTesting
+    public ProtoLogConfigurationServiceImpl(
+            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
+            @NonNull ViewerConfigFileTracer tracer) {
+        mDataSource = dataSourceBuilder.build(
+            this::onTracingInstanceStart,
+            this::onTracingInstanceFlush,
+            this::onTracingInstanceStop
+        );
+
+        // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be
+        // receive the lifecycle callbacks of the datasource and write the viewer configs if and
+        // when required to the datasource.
+        Producer.init(InitArguments.DEFAULTS);
+        final var params = new DataSourceParams.Builder()
+                .setBufferExhaustedPolicy(DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
+                .build();
+        mDataSource.register(params);
+
+        mViewerConfigFileTracer = tracer;
+    }
+
+    public static class RegisterClientArgs extends IRegisterClientArgs.Stub {
+        /**
+         * The viewer config file to be registered for this client ProtoLog process.
+         */
+        @Nullable
+        private String mViewerConfigFile = null;
+        /**
+         * The list of all groups that this client protolog process supports and might trace.
+         */
+        @NonNull
+        private String[] mGroups = new String[0];
+        /**
+         * The default logcat status of the ProtoLog client. True is logging to logcat, false
+         * otherwise. The indices should match the indices in {@link mGroups}.
+         */
+        @NonNull
+        private boolean[] mLogcatStatus = new boolean[0];
+
+        public record GroupConfig(@NonNull String group, boolean logToLogcat) {}
+
+        /**
+         * Specify groups to register with this client that will be used for protologging in this
+         * process.
+         * @param groups to register with this client.
+         * @return self
+         */
+        public RegisterClientArgs setGroups(GroupConfig... groups) {
+            mGroups = new String[groups.length];
+            mLogcatStatus = new boolean[groups.length];
+
+            for (int i = 0; i < groups.length; i++) {
+                mGroups[i] = groups[i].group;
+                mLogcatStatus[i] = groups[i].logToLogcat;
+            }
+
+            return this;
+        }
+
+        /**
+         * Set the viewer config file that the logs in this process are using.
+         * @param viewerConfigFile The file path of the viewer config.
+         * @return self
+         */
+        public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) {
+            mViewerConfigFile = viewerConfigFile;
+
+            return this;
+        }
+
+        @Override
+        @NonNull
+        public String[] getGroups() {
+            return mGroups;
+        }
+
+        @Override
+        @NonNull
+        public boolean[] getGroupsDefaultLogcatStatus() {
+            return mLogcatStatus;
+        }
+
+        @Nullable
+        @Override
+        public String getViewerConfigFile() {
+            return mViewerConfigFile;
+        }
+    }
+
+    @FunctionalInterface
+    public interface ViewerConfigFileTracer {
+        /**
+         * Write the viewer config data to the trace buffer.
+         *
+         * @param dataSource The target datasource to write the viewer config to.
+         * @param viewerConfigFilePath The path of the viewer config file which contains the data we
+         *                             want to write to the trace buffer.
+         * @throws FileNotFoundException if the viewerConfigFilePath is invalid.
+         */
+        void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath);
+    }
+
+    @Override
+    public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args)
+            throws RemoteException {
+        client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0);
+
+        final String viewerConfigFile = args.getViewerConfigFile();
+        if (viewerConfigFile != null) {
+            registerViewerConfigFile(client, viewerConfigFile);
+        }
+
+        registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
+    }
+
+    @Override
+    public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+            @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
+            @NonNull ResultReceiver resultReceiver) throws RemoteException {
+        new ProtoLogCommandHandler(this)
+                .exec(this, in, out, err, args, callback, resultReceiver);
+    }
+
+    /**
+     * Get the list of groups clients have registered to the protolog service.
+     * @return The list of ProtoLog groups registered with this service.
+     */
+    @Override
+    @NonNull
+    public String[] getGroups() {
+        return mRegisteredGroups.toArray(new String[0]);
+    }
+
+    /**
+     * Enable logging target groups to logcat.
+     * @param groups we want to enable logging them to logcat for.
+     */
+    @Override
+    public void enableProtoLogToLogcat(@NonNull String... groups) {
+        toggleProtoLogToLogcat(true, groups);
+    }
+
+    /**
+     * Disable logging target groups to logcat.
+     * @param groups we want to disable from being logged to logcat.
+     */
+    @Override
+    public void disableProtoLogToLogcat(@NonNull String... groups) {
+        toggleProtoLogToLogcat(false, groups);
+    }
+
+    /**
+     * Check if a group is logging to logcat
+     * @param group The group we want to check for
+     * @return True iff we are logging this group to logcat.
+     */
+    @Override
+    public boolean isLoggingToLogcat(@NonNull String group) {
+        final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group);
+
+        if (isLoggingToLogcat == null) {
+            throw new RuntimeException(
+                    "Trying to get logcat logging status of non-registered group " + group);
+        }
+
+        return isLoggingToLogcat;
+    }
+
+    private void registerViewerConfigFile(
+            @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) {
+        final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0);
+        mConfigFileCounts.put(viewerConfigFile, count + 1);
+        mClientConfigFiles.put(client, viewerConfigFile);
+    }
+
+    private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups,
+            @NonNull boolean[] logcatStatuses) throws RemoteException {
+        if (groups.length != logcatStatuses.length) {
+            throw new RuntimeException(
+                    "Expected groups and logcatStatuses to have the same length, "
+                        + "but groups has length " + groups.length
+                        + " and logcatStatuses has length " + logcatStatuses.length);
+        }
+
+        for (int i = 0; i < groups.length; i++) {
+            String group = groups[i];
+            boolean logcatStatus = logcatStatuses[i];
+
+            mRegisteredGroups.add(group);
+
+            mGroupToClients.putIfAbsent(group, new HashSet<>());
+            mGroupToClients.get(group).add(client);
+
+            if (!mLogGroupToLogcatStatus.containsKey(group)) {
+                mLogGroupToLogcatStatus.put(group, logcatStatus);
+            }
+
+            boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group);
+            if (requestedLogToLogcat != logcatStatus) {
+                client.toggleLogcat(requestedLogToLogcat, new String[] { group });
+            }
+        }
+    }
+
+    private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) {
+        final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>();
+
+        for (String group : groups) {
+            final var clients = mGroupToClients.get(group);
+
+            if (clients == null) {
+                // No clients associated to this group
+                Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group
+                        + " with no registered clients.");
+                continue;
+            }
+
+            for (IProtoLogClient client : clients) {
+                clientToGroups.putIfAbsent(client, new HashSet<>());
+                clientToGroups.get(client).add(group);
+            }
+        }
+
+        for (IProtoLogClient client : clientToGroups.keySet()) {
+            try {
+                client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0]));
+            } catch (RemoteException e) {
+                throw new RuntimeException(
+                        "Failed to toggle logcat status for groups on client", e);
+            }
+        }
+
+        for (String group : groups) {
+            mLogGroupToLogcatStatus.put(group, enabled);
+        }
+    }
+
+    private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+        mRunningInstances.add(instanceIdx);
+    }
+
+    private void onTracingInstanceFlush() {
+        for (String fileName : mConfigFileCounts.keySet()) {
+            mViewerConfigFileTracer.trace(mDataSource, fileName);
+        }
+    }
+
+    private void onTracingInstanceStop(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+        mRunningInstances.remove(instanceIdx);
+    }
+
+    private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource,
+            @NonNull String viewerConfigFilePath) {
+        Utils.dumpViewerConfig(dataSource, () -> {
+            try {
+                return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
+            } catch (FileNotFoundException e) {
+                throw new RuntimeException(
+                        "Failed to load viewer config file " + viewerConfigFilePath, e);
+            }
+        });
+    }
+
+    private void onClientBinderDeath(@NonNull IProtoLogClient client) {
+        // Dump the tracing config now if no other client is going to dump the same config file.
+        String configFile = mClientConfigFiles.get(client);
+        if (configFile != null) {
+            final var newCount = mConfigFileCounts.get(configFile) - 1;
+            mConfigFileCounts.put(configFile, newCount);
+            boolean lastProcessWithViewerConfig = newCount == 0;
+            if (lastProcessWithViewerConfig) {
+                mViewerConfigFileTracer.trace(mDataSource, configFile);
+            }
+        }
+    }
+
+    private static void writeViewerConfigGroup(
+            @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
+        final long inGroupToken = pis.start(GROUPS);
+        final long outGroupToken = os.start(GROUPS);
+
+        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (pis.getFieldNumber()) {
+                case (int) ID -> {
+                    int id = pis.readInt(ID);
+                    os.write(ID, id);
+                }
+                case (int) NAME -> {
+                    String name = pis.readString(NAME);
+                    os.write(NAME, name);
+                }
+                case (int) TAG -> {
+                    String tag = pis.readString(TAG);
+                    os.write(TAG, tag);
+                }
+                default ->
+                    throw new RuntimeException(
+                            "Unexpected field id " + pis.getFieldNumber());
+            }
+        }
+
+        pis.end(inGroupToken);
+        os.end(outGroupToken);
+    }
+
+    private static void writeViewerConfigMessage(
+            @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
+        final long inMessageToken = pis.start(MESSAGES);
+        final long outMessagesToken = os.start(MESSAGES);
+
+        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (pis.getFieldNumber()) {
+                case (int) MESSAGE_ID -> os.write(MESSAGE_ID,
+                        pis.readLong(MESSAGE_ID));
+                case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE));
+                case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL));
+                case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID));
+                case (int) LOCATION -> os.write(LOCATION, pis.readString(LOCATION));
+                default ->
+                    throw new RuntimeException(
+                            "Unexpected field id " + pis.getFieldNumber());
+            }
+        }
+
+        pis.end(inMessageToken);
+        os.end(outMessagesToken);
+    }
+}
diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java
index b73cacb..bdb33c4 100644
--- a/core/java/com/android/internal/view/menu/ListMenuItemView.java
+++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java
@@ -33,8 +33,6 @@
 import android.widget.RadioButton;
 import android.widget.TextView;
 
-import com.android.text.flags.Flags;
-
 /**
  * The item view for each item in the ListView-based MenuViews.
  */
@@ -283,10 +281,7 @@
 
     private void insertIconView() {
         LayoutInflater inflater = getInflater();
-        mIconView = (ImageView) inflater.inflate(
-                !Flags.fixMisalignedContextMenu()
-                        ? com.android.internal.R.layout.list_menu_item_fixed_size_icon :
-                        com.android.internal.R.layout.list_menu_item_icon,
+        mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon,
                 this, false);
         addContentView(mIconView, 0);
     }
diff --git a/core/java/com/android/internal/view/menu/StandardMenuPopup.java b/core/java/com/android/internal/view/menu/StandardMenuPopup.java
index c43a8c6..8e2536a 100644
--- a/core/java/com/android/internal/view/menu/StandardMenuPopup.java
+++ b/core/java/com/android/internal/view/menu/StandardMenuPopup.java
@@ -35,8 +35,6 @@
 import android.widget.PopupWindow.OnDismissListener;
 import android.widget.TextView;
 
-import com.android.text.flags.Flags;
-
 import java.util.Objects;
 
 /**
@@ -122,8 +120,7 @@
         mMenu = menu;
         mOverflowOnly = overflowOnly;
         final LayoutInflater inflater = LayoutInflater.from(context);
-        mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly,
-                Flags.fixMisalignedContextMenu() ? ITEM_LAYOUT_MATERIAL : ITEM_LAYOUT);
+        mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly, ITEM_LAYOUT_MATERIAL);
         mPopupStyleAttr = popupStyleAttr;
         mPopupStyleRes = popupStyleRes;
 
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index cb7c226..606e829 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -401,6 +401,7 @@
     optional SettingProto long_press_timeout = 35 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto key_press_timeout_ms = 96 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto key_press_delay_ms = 97 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    optional SettingProto key_repeat_enabled = 102 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     message ManagedProfile {
         option (android.msg_privacy).dest = DEST_EXPLICIT;
@@ -735,5 +736,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 102;
+    // Next tag = 103;
 }
diff --git a/core/tests/coretests/src/android/tracing/OWNERS b/core/tests/coretests/src/android/tracing/OWNERS
deleted file mode 100644
index 86a7e88..0000000
--- a/core/tests/coretests/src/android/tracing/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include platform/development:/tools/winscope/OWNERS
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/tracing/TEST_MAPPING b/core/tests/coretests/src/android/tracing/TEST_MAPPING
deleted file mode 100644
index 4b7adf9..0000000
--- a/core/tests/coretests/src/android/tracing/TEST_MAPPING
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "postsubmit": [
-    {
-       "name": "FrameworksCoreTests_android_tracing",
-        "file_patterns": [".*\\.java"]
-    }
-  ]
-}
diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java
index 6d31578..e07471c 100644
--- a/graphics/java/android/graphics/Bitmap.java
+++ b/graphics/java/android/graphics/Bitmap.java
@@ -128,6 +128,22 @@
     private static final WeakHashMap<Bitmap, Void> sAllBitmaps = new WeakHashMap<>();
 
     /**
+     * @hide
+     */
+    private static NativeAllocationRegistry getRegistry(boolean malloc, long size) {
+        final long free = nativeGetNativeFinalizer();
+        if (com.android.libcore.Flags.nativeMetrics()) {
+            Class cls = Bitmap.class;
+            return malloc ? NativeAllocationRegistry.createMalloced(cls, free, size)
+                          : NativeAllocationRegistry.createNonmalloced(cls, free, size);
+        } else {
+            ClassLoader loader = Bitmap.class.getClassLoader();
+            return malloc ? NativeAllocationRegistry.createMalloced(loader, free, size)
+                          : NativeAllocationRegistry.createNonmalloced(loader, free, size);
+        }
+    }
+
+    /**
      * Private constructor that must receive an already allocated native bitmap
      * int (pointer).
      */
@@ -151,7 +167,6 @@
         mWidth = width;
         mHeight = height;
         mRequestPremultiplied = requestPremultiplied;
-
         mNinePatchChunk = ninePatchChunk;
         mNinePatchInsets = ninePatchInsets;
         if (density >= 0) {
@@ -159,17 +174,9 @@
         }
 
         mNativePtr = nativeBitmap;
-
         final int allocationByteCount = getAllocationByteCount();
-        NativeAllocationRegistry registry;
-        if (fromMalloc) {
-            registry = NativeAllocationRegistry.createMalloced(
-                    Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
-        } else {
-            registry = NativeAllocationRegistry.createNonmalloced(
-                    Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
-        }
-        registry.registerNativeAllocation(this, nativeBitmap);
+        getRegistry(fromMalloc, allocationByteCount).registerNativeAllocation(this, mNativePtr);
+
         synchronized (Bitmap.class) {
           sAllBitmaps.put(this, null);
         }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index dd86a1a..83619ef 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -90,9 +90,6 @@
     /** The maximum override density allowed for tasks inside the desktop. */
     private static final int DESKTOP_DENSITY_MAX = 1000;
 
-    /** The number of [WindowDecorViewHost] instances to warm up on system start. */
-    private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2;
-
     /**
      * Sysprop declaring whether to enters desktop mode by default when the windowing mode of the
      * display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM.
@@ -115,14 +112,6 @@
     private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";
 
     /**
-     * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start.
-     *
-     * <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
-     */
-    private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
-            "persist.wm.debug.desktop_window_decor_pre_warm_size";
-
-    /**
      * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
      */
     public static boolean isVeiledResizeEnabled() {
@@ -162,12 +151,6 @@
                 context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
     }
 
-    /** The number of [WindowDecorViewHost] instances to warm up on system start. */
-    public static int getWindowDecorPreWarmSize() {
-        return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP,
-                WINDOW_DECOR_PRE_WARM_SIZE);
-    }
-
     /**
      * Return {@code true} if the current device supports desktop mode.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index c545d73..af4a0c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1241,8 +1241,9 @@
             mBubbleData.dismissBubbleWithKey(
                     bubbleKey, Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER, timestamp);
         }
-        if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) {
-            // We did not remove the selected bubble. Expand it again
+        if (mBubbleData.hasBubbles()) {
+            // We still have bubbles, if we dragged an individual bubble to dismiss we were expanded
+            // so re-expand to whatever is selected.
             showExpandedViewForBubbleBar();
         }
     }
@@ -2007,7 +2008,7 @@
         @Override
         public void selectionChanged(BubbleViewProvider selectedBubble) {
             // Only need to update the layer view if we're currently expanded for selection changes.
-            if (mLayerView != null && isStackExpanded()) {
+            if (mLayerView != null && mLayerView.isExpanded()) {
                 mLayerView.showExpandedView(selectedBubble);
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 1c9c195..1367b7e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -186,6 +186,10 @@
         if (expandedView == null) {
             return;
         }
+        if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) {
+            // Already showing this bubble, skip animating
+            return;
+        }
         if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
             removeView(mExpandedView);
             mExpandedView = null;
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 b47adb4..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;
@@ -116,8 +117,6 @@
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 import com.android.wm.shell.windowdecor.viewhost.DefaultWindowDecorViewHostSupplier;
-import com.android.wm.shell.windowdecor.viewhost.PooledWindowDecorViewHostSupplier;
-import com.android.wm.shell.windowdecor.viewhost.ReusableWindowDecorViewHost;
 import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
 
 import dagger.Binds;
@@ -143,7 +142,7 @@
         includes = {
                 WMShellBaseModule.class,
                 PipModule.class,
-                ShellBackAnimationModule.class,
+                ShellBackAnimationModule.class
         })
 public abstract class WMShellModule {
 
@@ -249,6 +248,7 @@
             AssistContentRequester assistContentRequester,
             MultiInstanceHelper multiInstanceHelper,
             Optional<DesktopTasksLimiter> desktopTasksLimiter,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
             WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
@@ -274,6 +274,7 @@
                     assistContentRequester,
                     multiInstanceHelper,
                     desktopTasksLimiter,
+                    windowDecorCaptionHandleRepository,
                     desktopActivityOrientationHandler,
                     windowDecorViewHostSupplier);
         }
@@ -384,19 +385,8 @@
     @WMSingleton
     @Provides
     static WindowDecorViewHostSupplier provideWindowDecorViewHostSupplier(
-            @NonNull Context context,
-            @ShellMainThread @NonNull CoroutineScope mainScope,
-            @NonNull ShellInit shellInit) {
-        if (DesktopModeStatus.canEnterDesktopMode(context)
-                && Flags.enableDesktopWindowingScvhCache()) {
-            final int maxPoolSize = DesktopModeStatus.getMaxTaskLimit(context);
-            final int preWarmSize = DesktopModeStatus.getWindowDecorPreWarmSize();
-            return new PooledWindowDecorViewHostSupplier(
-                    context, mainScope, shellInit,
-                    ReusableWindowDecorViewHost.DefaultFactory.INSTANCE, maxPoolSize, preWarmSize);
-        } else {
-            return new DefaultWindowDecorViewHostSupplier(mainScope);
-        }
+            @ShellMainThread @NonNull CoroutineScope mainScope) {
+        return new DefaultWindowDecorViewHostSupplier(mainScope);
     }
 
     //
@@ -793,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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 853284a..b8ebbcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -305,13 +305,18 @@
     private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) =
         if (task1.taskId == task2.parentTaskId) task2 else task1
 
-    private fun isFreeformDisplay(displayId: Int): Boolean {
+    private fun forceEnterDesktop(displayId: Int): Boolean {
+        if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) {
+            return false
+        }
+
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
         requireNotNull(tdaInfo) {
             "This method can only be called with the ID of a display having non-null DisplayArea."
         }
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
-        return tdaWindowingMode == WINDOWING_MODE_FREEFORM
+        val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM
+        return isFreeformDisplay
     }
 
     /** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@@ -1191,10 +1196,11 @@
         val wct = WindowContainerTransaction()
         if (!isDesktopModeShowing(task.displayId)) {
             logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId)
-            // We are outside of desktop mode and already existing desktop task is being launched.
-            // We should make this task go to fullscreen instead of freeform. Note that this means
-            // any re-launch of a freeform window outside of desktop will be in fullscreen.
-            if (taskRepository.isActiveTask(task.taskId)) {
+            if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) {
+                // We are outside of desktop mode and already existing desktop task is being
+                // launched. We should make this task go to fullscreen instead of freeform. Note
+                // that this means any re-launch of a freeform window outside of desktop will be in
+                // fullscreen as long as default-desktop flag is disabled.
                 addMoveToFullscreenChanges(wct, task)
                 return wct
             }
@@ -1231,9 +1237,7 @@
         transition: IBinder
     ): WindowContainerTransaction? {
         logV("handleFullscreenTaskLaunch")
-        val forceEnterDesktop = DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context) &&
-                isFreeformDisplay(task.displayId)
-        if (isDesktopModeShowing(task.displayId) || forceEnterDesktop) {
+        if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) {
             logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId)
             return WindowContainerTransaction().also { wct ->
                 addMoveToDesktopChanges(wct, task)
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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 5a905cf..e8eb10c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2456,6 +2456,7 @@
             final StageChangeRecord record = new StageChangeRecord();
             final int transitType = info.getType();
             TransitionInfo.Change pipChange = null;
+            int closingSplitTaskId = -1;
             for (int iC = 0; iC < info.getChanges().size(); ++iC) {
                 final TransitionInfo.Change change = info.getChanges().get(iC);
                 if (change.getMode() == TRANSIT_CHANGE
@@ -2516,21 +2517,31 @@
                                 + " with " + taskInfo.taskId + " before startAnimation().");
                     }
                 }
+                if (isClosingType(change.getMode()) &&
+                        getStageOfTask(change.getTaskInfo().taskId) != STAGE_TYPE_UNDEFINED) {
+                    // If either one of the 2 stages is closing we're assuming we'll break split
+                    closingSplitTaskId = change.getTaskInfo().taskId;
+                }
             }
 
             if (pipChange != null) {
                 TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange,
                         mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId,
                         getSplitItemStage(pipChange.getLastParent()));
-                if (pipReplacingChange != null) {
+                boolean keepSplitWithPip = pipReplacingChange != null && closingSplitTaskId == -1;
+                if (keepSplitWithPip) {
                     // Set an enter transition for when startAnimation gets called again
                     mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null,
                             TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false);
+                } else {
+                    int finalClosingTaskId = closingSplitTaskId;
+                    mRecentTasks.ifPresent(recentTasks ->
+                            recentTasks.removeSplitPair(finalClosingTaskId));
+                    logExit(EXIT_REASON_FULLSCREEN_REQUEST);
                 }
 
                 mMixedHandler.animatePendingEnterPipFromSplit(transition, info,
-                        startTransaction, finishTransaction, finishCallback,
-                        pipReplacingChange != null);
+                        startTransaction, finishTransaction, finishCallback, keepSplitWithPip);
                 notifySplitAnimationFinished();
                 return true;
             }
@@ -2821,8 +2832,12 @@
             }
             callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
             mWindowDecorViewModel.ifPresent(viewModel -> {
-                viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo());
-                viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo());
+                if (finalMainChild != null) {
+                    viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo());
+                }
+                if (finalSideChild != null) {
+                    viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo());
+                }
             });
             mPausingTasks.clear();
         });
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 02c818f..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) -> {
@@ -1191,8 +1201,10 @@
                                                 : SPLIT_POSITION_TOP_OR_LEFT;
                                 final RunningTaskInfo oppositeTaskInfo =
                                         mSplitScreenController.getTaskInfo(oppositePosition);
-                                mWindowDecorByTaskId.get(oppositeTaskInfo.taskId)
-                                        .disposeStatusBarInputLayer();
+                                if (oppositeTaskInfo != null) {
+                                    mWindowDecorByTaskId.get(oppositeTaskInfo.taskId)
+                                            .disposeStatusBarInputLayer();
+                                }
                             }
                         }
                         mMoveToDesktopAnimator = null;
@@ -1375,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/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 6f3f411..6eb5cca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -26,6 +26,8 @@
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
+import android.view.Choreographer;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
@@ -124,6 +126,11 @@
 
     @Override
     public Rect onDragPositioningMove(float x, float y) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            // This method must run on the shell main thread to use the correct Choreographer
+            // instance below.
+            throw new IllegalStateException("This method must run on the shell main thread.");
+        }
         PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint);
         if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType,
                 mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
@@ -141,6 +148,7 @@
             final SurfaceControl.Transaction t = mTransactionSupplier.get();
             DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration,
                     mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y);
+            t.setFrameTimeline(Choreographer.getInstance().getVsyncId());
             t.apply();
         }
         return new Rect(mRepositionTaskBounds);
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/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
index 5156e47..139e679 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
@@ -19,33 +19,51 @@
 import android.content.res.Configuration
 import android.view.Display
 import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
 import android.view.View
 import android.view.WindowManager
+import android.view.WindowlessWindowManager
 import androidx.tracing.Trace
 import com.android.internal.annotations.VisibleForTesting
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
+typealias SurfaceControlViewHostFactory =
+            (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost
 
 /**
- * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHostAdapter].
+ * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHost].
  *
- * It supports asynchronously updating the view hierarchy using [updateViewAsync], in which
+ * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
+ * any attempts to do will throw, which means that once a [View] is added using [updateView] or
+ * [updateViewAsync], only its properties and binding may be changed, its children views may be
+ * added, removed or changed and its [WindowManager.LayoutParams] may be changed.
+ * It also supports asynchronously updating the view hierarchy using [updateViewAsync], in which
  * case the update work will be posted on the [ShellMainThread] with no delay.
  */
 class DefaultWindowDecorViewHost(
-    context: Context,
+    private val context: Context,
     @ShellMainThread private val mainScope: CoroutineScope,
-    display: Display,
-    @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
-        SurfaceControlViewHostAdapter(context, display)
+    private val display: Display,
+    private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
+        SurfaceControlViewHost(c, d, wwm, s)
+    }
 ) : WindowDecorViewHost {
 
+    private val rootSurface: SurfaceControl = SurfaceControl.Builder()
+            .setName("DefaultWindowDecorViewHost surface")
+            .setContainerLayer()
+            .setCallsite("DefaultWindowDecorViewHost#init")
+            .build()
+
+    private var wwm: WindowlessWindowManager? = null
+    @VisibleForTesting
+    var viewHost: SurfaceControlViewHost? = null
     private var currentUpdateJob: Job? = null
 
     override val surfaceControl: SurfaceControl
-        get() = viewHostAdapter.rootSurface
+        get() = rootSurface
 
     override fun updateView(
         view: View,
@@ -74,7 +92,8 @@
 
     override fun release(t: SurfaceControl.Transaction) {
         clearCurrentUpdateJob()
-        viewHostAdapter.release(t)
+        viewHost?.release()
+        t.remove(rootSurface)
     }
 
     private fun updateViewHost(
@@ -83,15 +102,45 @@
         configuration: Configuration,
         onDrawTransaction: SurfaceControl.Transaction?
     ) {
-        viewHostAdapter.prepareViewHost(configuration)
-        onDrawTransaction?.let {
-            viewHostAdapter.applyTransactionOnDraw(it)
+        Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost")
+        if (wwm == null) {
+            wwm = WindowlessWindowManager(configuration, rootSurface, null)
         }
-        viewHostAdapter.updateView(view, attrs)
+        requireWindowlessWindowManager().setConfiguration(configuration)
+        if (viewHost == null) {
+            viewHost = surfaceControlViewHostFactory.invoke(
+                context,
+                display,
+                requireWindowlessWindowManager(),
+                "DefaultWindowDecorViewHost#updateViewHost"
+            )
+        }
+        onDrawTransaction?.let {
+            requireViewHost().rootSurfaceControl.applyTransactionOnDraw(it)
+        }
+        if (requireViewHost().view == null) {
+            Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-setView")
+            requireViewHost().setView(view, attrs)
+            Trace.endSection()
+        } else {
+            check(requireViewHost().view == view) { "Changing view is not allowed" }
+            Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-relayout")
+            requireViewHost().relayout(attrs)
+            Trace.endSection()
+        }
+        Trace.endSection()
     }
 
     private fun clearCurrentUpdateJob() {
         currentUpdateJob?.cancel()
         currentUpdateJob = null
     }
+
+    private fun requireWindowlessWindowManager(): WindowlessWindowManager {
+        return wwm ?: error("Expected non-null windowless window manager")
+    }
+
+    private fun requireViewHost(): SurfaceControlViewHost {
+        return viewHost ?: error("Expected non-null view host")
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt
deleted file mode 100644
index b04188f..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt
+++ /dev/null
@@ -1,105 +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.wm.shell.windowdecor.viewhost
-
-import android.content.Context
-import android.os.Trace
-import android.util.Pools
-import android.view.Display
-import android.view.SurfaceControl
-import com.android.wm.shell.shared.annotations.ShellMainThread
-import com.android.wm.shell.sysui.ShellInit
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-/**
- * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be
- * expensive to recreate for each new/updated window decoration.
- *
- * Callers can obtain [ReusableWindowDecorViewHost] using [acquire], which will return a pooled
- * object if available, or create a new instance and return it if needed. When done using a
- * [ReusableWindowDecorViewHost], it must be released using [release] to allow it to be sent back
- * into the pool and reused later on.
- *
- * This class also supports pre-warming [ReusableWindowDecorViewHost] instances, which will be put
- * into the pool immediately after creation.
- */
-class PooledWindowDecorViewHostSupplier(
-    private val context: Context,
-    @ShellMainThread private val mainScope: CoroutineScope,
-    shellInit: ShellInit,
-    private val viewHostFactory: ReusableWindowDecorViewHost.Factory =
-        ReusableWindowDecorViewHost.DefaultFactory,
-    maxPoolSize: Int,
-    private val preWarmSize: Int,
-) : WindowDecorViewHostSupplier<ReusableWindowDecorViewHost> {
-
-    private val pool: Pools.Pool<ReusableWindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize)
-    private var nextDecorViewHostId = 0
-
-    init {
-        require(preWarmSize <= maxPoolSize) { "Pre-warm size should not exceed pool size" }
-        shellInit.addInitCallback(this::onShellInit, this)
-    }
-
-    private fun onShellInit() {
-        if (preWarmSize <= 0) {
-            return
-        }
-        preWarmViewHosts(preWarmSize)
-    }
-
-    private fun preWarmViewHosts(preWarmSize: Int) {
-        mainScope.launch {
-            // Applying isn't needed, as the surface was never actually shown.
-            val t = SurfaceControl.Transaction()
-            repeat(preWarmSize) {
-                val warmedViewHost = create(context, context.display).apply {
-                    warmUp()
-                }
-                // Put the warmed view host in the pool by releasing it.
-                release(warmedViewHost, t)
-            }
-        }
-    }
-
-    override fun acquire(context: Context, display: Display): ReusableWindowDecorViewHost {
-        val reusedDecorViewHost = pool.acquire()
-        if (reusedDecorViewHost != null) {
-            return reusedDecorViewHost
-        }
-        Trace.beginSection("WindowDecorViewHostPool#acquire-new")
-        val newDecorViewHost = create(context, display)
-        Trace.endSection()
-        return newDecorViewHost
-    }
-
-    override fun release(viewHost: ReusableWindowDecorViewHost, t: SurfaceControl.Transaction) {
-        val cached = pool.release(viewHost)
-        if (!cached) {
-            viewHost.release(t)
-        }
-    }
-
-    private fun create(context: Context, display: Display): ReusableWindowDecorViewHost {
-        return viewHostFactory.create(
-            context,
-            mainScope,
-            display,
-            nextDecorViewHostId++
-        )
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt
deleted file mode 100644
index 64536d1..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt
+++ /dev/null
@@ -1,161 +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.wm.shell.windowdecor.viewhost
-
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.PixelFormat
-import android.os.Trace
-import android.view.Display
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.View
-import android.view.WindowManager
-import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
-import android.view.WindowManager.LayoutParams.TYPE_APPLICATION
-import android.widget.FrameLayout
-import com.android.internal.annotations.VisibleForTesting
-import com.android.wm.shell.shared.annotations.ShellMainThread
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-
-/**
- * An implementation of [WindowDecorViewHost] that supports:
- *  1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be
- *    called with different [View] instances. This is useful when reusing [WindowDecorViewHost]s
- *    instances for vastly different view hierarchies, such as Desktop Windowing's App Handles and
- *    App Headers.
- *  2) Pre-warming of the underlying [SurfaceControlViewHost]s. Useful because their creation and
- *    first root view assignment are expensive, which is undesirable in latency-sensitive code
- *    paths like during a shell transition.
- */
-class ReusableWindowDecorViewHost(
-    private val context: Context,
-    @ShellMainThread private val mainScope: CoroutineScope,
-    display: Display,
-    val id: Int,
-    @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
-        SurfaceControlViewHostAdapter(context, display)
-) : WindowDecorViewHost, Warmable {
-
-    @VisibleForTesting
-    val rootView = FrameLayout(context)
-
-    private var currentUpdateJob: Job? = null
-
-    override val surfaceControl: SurfaceControl
-        get() = viewHostAdapter.rootSurface
-
-    override fun warmUp() {
-        if (viewHostAdapter.isInitialized()) {
-            // Already warmed up.
-            return
-        }
-        Trace.beginSection("$TAG#warmUp")
-        viewHostAdapter.prepareViewHost(context.resources.configuration)
-        viewHostAdapter.updateView(
-            rootView,
-            WindowManager.LayoutParams(
-                0 /* width*/,
-                0 /* height */,
-                TYPE_APPLICATION,
-                FLAG_NOT_FOCUSABLE or FLAG_SPLIT_TOUCH,
-                PixelFormat.TRANSPARENT
-            ).apply {
-                setTitle("View root of $TAG#$id")
-                setTrustedOverlay()
-            }
-        )
-        Trace.endSection()
-    }
-
-    override fun updateView(
-        view: View,
-        attrs: WindowManager.LayoutParams,
-        configuration: Configuration,
-        onDrawTransaction: SurfaceControl.Transaction?
-    ) {
-        clearCurrentUpdateJob()
-        updateViewHost(view, attrs, configuration, onDrawTransaction)
-    }
-
-    override fun updateViewAsync(
-        view: View,
-        attrs: WindowManager.LayoutParams,
-        configuration: Configuration
-    ) {
-        clearCurrentUpdateJob()
-        currentUpdateJob = mainScope.launch {
-            updateViewHost(view, attrs, configuration, onDrawTransaction = null)
-        }
-    }
-
-    override fun release(t: SurfaceControl.Transaction) {
-        clearCurrentUpdateJob()
-        viewHostAdapter.release(t)
-    }
-
-    private fun updateViewHost(
-        view: View,
-        attrs: WindowManager.LayoutParams,
-        configuration: Configuration,
-        onDrawTransaction: SurfaceControl.Transaction?
-    ) {
-        viewHostAdapter.prepareViewHost(configuration)
-        onDrawTransaction?.let {
-            viewHostAdapter.applyTransactionOnDraw(it)
-        }
-        rootView.removeAllViews()
-        rootView.addView(view)
-        viewHostAdapter.updateView(rootView, attrs)
-    }
-
-    private fun clearCurrentUpdateJob() {
-        currentUpdateJob?.cancel()
-        currentUpdateJob = null
-    }
-
-    interface Factory {
-        fun create(
-            context: Context,
-            @ShellMainThread mainScope: CoroutineScope,
-            display: Display,
-            id: Int
-        ): ReusableWindowDecorViewHost
-    }
-
-    object DefaultFactory : Factory {
-        override fun create(
-            context: Context,
-            @ShellMainThread mainScope: CoroutineScope,
-            display: Display,
-            id: Int
-        ): ReusableWindowDecorViewHost {
-            return ReusableWindowDecorViewHost(
-                context,
-                mainScope,
-                display,
-                id
-            )
-        }
-    }
-
-    companion object {
-        private const val TAG = "ReusableWindowDecorViewHost"
-    }
-}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt
deleted file mode 100644
index a54c9ba..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt
+++ /dev/null
@@ -1,111 +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.wm.shell.windowdecor.viewhost
-
-import android.content.Context
-import android.content.res.Configuration
-import android.view.AttachedSurfaceControl
-import android.view.Display
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.View
-import android.view.WindowManager
-import android.view.WindowlessWindowManager
-import androidx.tracing.Trace
-import com.android.internal.annotations.VisibleForTesting
-typealias SurfaceControlViewHostFactory =
-            (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost
-
-/**
- * Adapter for a [SurfaceControlViewHost] and its backing [SurfaceControl].
- *
- * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
- * any attempts to do will throw, which means that once a [View] is added using [updateView], only
- * its properties and binding may be changed, its children views may be added, removed or changed
- * and its [WindowManager.LayoutParams] may be changed.
- */
-class SurfaceControlViewHostAdapter(
-    private val context: Context,
-    private val display: Display,
-    private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
-        SurfaceControlViewHost(c, d, wwm, s)
-    }
-) {
-    val rootSurface: SurfaceControl = SurfaceControl.Builder()
-        .setName("SurfaceControlViewHostAdapter surface")
-        .setContainerLayer()
-        .setCallsite("SurfaceControlViewHostAdapter#init")
-        .build()
-
-    private var wwm: WindowlessWindowManager? = null
-    @VisibleForTesting
-    var viewHost: SurfaceControlViewHost? = null
-
-    /** Initialize the [SurfaceControlViewHost] if needed. */
-    fun prepareViewHost(configuration: Configuration) {
-        if (wwm == null) {
-            wwm = WindowlessWindowManager(configuration, rootSurface, null)
-        }
-        requireWindowlessWindowManager().setConfiguration(configuration)
-        if (viewHost == null) {
-            viewHost = surfaceControlViewHostFactory.invoke(
-                context,
-                display,
-                requireWindowlessWindowManager(),
-                "SurfaceControlViewHostAdapter#prepareViewHost"
-            )
-        }
-    }
-
-    /**
-     * Request to apply the transaction atomically with the next draw of the view hierarchy.
-     * See [AttachedSurfaceControl.applyTransactionOnDraw].
-     */
-    fun applyTransactionOnDraw(t: SurfaceControl.Transaction) {
-        requireViewHost().rootSurfaceControl.applyTransactionOnDraw(t)
-    }
-
-    /** Update the view hierarchy of the view host. */
-    fun updateView(view: View, attrs: WindowManager.LayoutParams) {
-        if (requireViewHost().view == null) {
-            Trace.beginSection("SurfaceControlViewHostAdapter#updateView-setView")
-            requireViewHost().setView(view, attrs)
-            Trace.endSection()
-        } else {
-            check(requireViewHost().view == view) { "Changing view is not allowed" }
-            Trace.beginSection("SurfaceControlViewHostAdapter#updateView-relayout")
-            requireViewHost().relayout(attrs)
-            Trace.endSection()
-        }
-    }
-
-    /** Release the view host and remove the backing surface. */
-    fun release(t: SurfaceControl.Transaction) {
-        viewHost?.release()
-        t.remove(rootSurface)
-    }
-
-    /** Whether the view host has had a view hierarchy set. */
-    fun isInitialized(): Boolean = viewHost?.view != null
-
-    private fun requireWindowlessWindowManager(): WindowlessWindowManager {
-        return wwm ?: error("Expected non-null windowless window manager")
-    }
-
-    private fun requireViewHost(): SurfaceControlViewHost {
-        return viewHost ?: error("Expected non-null view host")
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt
deleted file mode 100644
index 0df9bfa..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt
+++ /dev/null
@@ -1,23 +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.wm.shell.windowdecor.viewhost
-
-/**
- * An interface for an object that can be warmed up before it's needed.
- */
-interface Warmable {
-    fun warmUp()
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index e610ebd..8f20841 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1764,6 +1764,37 @@
   }
 
   @Test
+  fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() {
+    assumeTrue(ENABLE_SHELL_TRANSITIONS)
+    whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+    val freeformTask = setUpFreeformTask()
+    markTaskHidden(freeformTask)
+    val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+    assertNotNull(wct, "should handle request")
+    assertFalse(wct.anyWindowingModeChange(freeformTask.token))
+  }
+
+  @Test
+  fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() {
+    assumeTrue(ENABLE_SHELL_TRANSITIONS)
+    whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+    val freeformTask = setUpFreeformTask()
+    markTaskHidden(freeformTask)
+    val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+    assertNotNull(wct, "should handle request")
+    assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
+      .isEqualTo(WINDOWING_MODE_UNDEFINED)
+  }
+
+  @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
     assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -3493,6 +3524,14 @@
   } ?: false
 }
 
+private fun WindowContainerTransaction?.anyWindowingModeChange(
+  token: WindowContainerToken
+): Boolean {
+return this?.changes?.any { change ->
+  change.key == token.asBinder() && change.value.windowingMode >= 0
+} ?: false
+}
+
 private fun createTaskInfo(id: Int) =
     RecentTaskInfo().apply {
       taskId = id
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/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/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/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index ab41d9c..1273ee8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -23,6 +23,7 @@
 import android.graphics.Rect
 import android.os.Handler
 import android.os.IBinder
+import android.os.Looper
 import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.view.Surface.ROTATION_0
@@ -34,6 +35,7 @@
 import android.window.TransitionInfo
 import android.window.WindowContainerToken
 import androidx.test.filters.SmallTest
+import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
@@ -108,8 +110,7 @@
     private lateinit var mockResources: Resources
     @Mock
     private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
-    @Mock
-    private lateinit var mockHandler: Handler
+    private val mainHandler = Handler(Looper.getMainLooper())
 
     private lateinit var taskPositioner: VeiledResizeTaskPositioner
 
@@ -159,12 +160,12 @@
                         mockTransactionFactory,
                         mockTransitions,
                         mockInteractionJankMonitor,
-                        mockHandler,
+                        mainHandler,
                 )
     }
 
     @Test
-    fun testDragResize_noMove_doesNotShowResizeVeil() {
+    fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
             STARTING_BOUNDS.left.toFloat(),
@@ -176,6 +177,7 @@
             STARTING_BOUNDS.left.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
+
         verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
                 token == taskBinder &&
@@ -186,7 +188,7 @@
     }
 
     @Test
-    fun testDragResize_movesTask_doesNotShowResizeVeil() {
+    fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_UNDEFINED,
             STARTING_BOUNDS.left.toFloat(),
@@ -221,7 +223,7 @@
     }
 
     @Test
-    fun testDragResize_resize_boundsUpdateOnEnd() {
+    fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
             STARTING_BOUNDS.right.toFloat(),
@@ -262,7 +264,7 @@
     }
 
     @Test
-    fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() {
+    fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
             STARTING_BOUNDS.left.toFloat(),
@@ -294,9 +296,8 @@
         })
     }
 
-
     @Test
-    fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() {
+    fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // drag
                 STARTING_BOUNDS.left.toFloat(),
@@ -321,7 +322,7 @@
     }
 
     @Test
-    fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() {
+    fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread {
         mockDesktopWindowDecoration.mTaskInfo.isFocused = false
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
@@ -337,7 +338,7 @@
     }
 
     @Test
-    fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() {
+    fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread {
         mockDesktopWindowDecoration.mTaskInfo.isFocused = true
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
@@ -353,7 +354,7 @@
     }
 
     @Test
-    fun testDragResize_drag_draggedTaskNotReorderedToTop() {
+    fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread {
         mockDesktopWindowDecoration.mTaskInfo.isFocused = false
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // drag
@@ -370,7 +371,7 @@
     }
 
     @Test
-    fun testDragResize_drag_updatesStableBoundsOnRotate() {
+    fun testDragResize_drag_updatesStableBoundsOnRotate() = runOnUiThread {
         // Test landscape stable bounds
         performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
             STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000,
@@ -416,7 +417,7 @@
     }
 
     @Test
-    fun testIsResizingOrAnimatingResizeSet() {
+    fun testIsResizingOrAnimatingResizeSet() = runOnUiThread {
         Assert.assertFalse(taskPositioner.isResizingOrAnimating)
 
         taskPositioner.onDragPositioningStart(
@@ -443,7 +444,7 @@
     }
 
     @Test
-    fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() {
+    fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() = runOnUiThread {
         performDrag(
                 STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat(),
                 STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20,
@@ -457,7 +458,7 @@
     }
 
     @Test
-    fun testStartAnimation_useEndRelOffset() {
+    fun testStartAnimation_useEndRelOffset() = runOnUiThread {
         val changeMock = mock(TransitionInfo.Change::class.java)
         val startTransaction = mock(Transaction::class.java)
         val finishTransaction = mock(Transaction::class.java)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
index 1b0b7d9..1b2ce9e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
@@ -18,6 +18,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
 import android.view.View
 import android.view.WindowManager
 import androidx.test.filters.SmallTest
@@ -27,6 +28,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -57,8 +59,54 @@
             onDrawTransaction = null
         )
 
-        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(windowDecorViewHost.view()).isEqualTo(view)
+        assertThat(windowDecorViewHost.viewHost).isNotNull()
+        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
+    }
+
+    @Test
+    fun updateView_alreadyLaidOut_relayouts() = runTest {
+        val windowDecorViewHost = createDefaultViewHost()
+        val view = View(context)
+        windowDecorViewHost.updateView(
+            view = view,
+            attrs = WindowManager.LayoutParams(100, 100),
+            configuration = context.resources.configuration,
+            onDrawTransaction = null
+        )
+
+        val otherParams = WindowManager.LayoutParams(200, 200)
+        windowDecorViewHost.updateView(
+            view = view,
+            attrs = otherParams,
+            configuration = context.resources.configuration,
+            onDrawTransaction = null
+        )
+
+        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
+        assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
+            .isEqualTo(otherParams.width)
+    }
+
+    @Test
+    fun updateView_replacingView_throws() = runTest {
+        val windowDecorViewHost = createDefaultViewHost()
+        val view = View(context)
+        windowDecorViewHost.updateView(
+            view = view,
+            attrs = WindowManager.LayoutParams(100, 100),
+            configuration = context.resources.configuration,
+            onDrawTransaction = null
+        )
+
+        val otherView = View(context)
+        assertThrows(Exception::class.java) {
+            windowDecorViewHost.updateView(
+                view = otherView,
+                attrs = WindowManager.LayoutParams(100, 100),
+                configuration = context.resources.configuration,
+                onDrawTransaction = null
+            )
+        }
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
@@ -77,7 +125,7 @@
         )
 
         // No view host yet, since the coroutine hasn't run.
-        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()
+        assertThat(windowDecorViewHost.viewHost).isNull()
 
         windowDecorViewHost.updateView(
             view = syncView,
@@ -89,13 +137,14 @@
         // Would run coroutine if it hadn't been cancelled.
         advanceUntilIdle()
 
-        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(windowDecorViewHost.view()).isNotNull()
+        assertThat(windowDecorViewHost.viewHost).isNotNull()
+        assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
         // View host view/attrs should match the ones from the sync call, plus, since the
         // sync/async were made with different views, if the job hadn't been cancelled there
         // would've been an exception thrown as replacing views isn't allowed.
-        assertThat(windowDecorViewHost.view()).isEqualTo(syncView)
-        assertThat(windowDecorViewHost.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
+        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(syncView)
+        assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
+            .isEqualTo(syncAttrs.width)
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
@@ -111,11 +160,11 @@
             configuration = context.resources.configuration,
         )
 
-        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()
+        assertThat(windowDecorViewHost.viewHost).isNull()
 
         advanceUntilIdle()
 
-        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
+        assertThat(windowDecorViewHost.viewHost).isNotNull()
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
@@ -138,8 +187,9 @@
 
         advanceUntilIdle()
 
-        assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(windowDecorViewHost.view()).isEqualTo(otherView)
+        assertThat(windowDecorViewHost.viewHost).isNotNull()
+        assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
+        assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(otherView)
     }
 
     @Test
@@ -157,15 +207,16 @@
         val t = mock(SurfaceControl.Transaction::class.java)
         windowDecorViewHost.release(t)
 
-        verify(windowDecorViewHost.viewHostAdapter).release(t)
+        verify(windowDecorViewHost.viewHost!!).release()
+        verify(t).remove(windowDecorViewHost.surfaceControl)
     }
 
     private fun CoroutineScope.createDefaultViewHost() = DefaultWindowDecorViewHost(
         context = context,
         mainScope = this,
         display = context.display,
-        viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+        surfaceControlViewHostFactory = { c, d, wwm, s ->
+            spy(SurfaceControlViewHost(c, d, wwm, s))
+        }
     )
-
-    private fun DefaultWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt
deleted file mode 100644
index a7e4213..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt
+++ /dev/null
@@ -1,181 +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.wm.shell.windowdecor.viewhost
-
-import android.testing.AndroidTestingRunner
-import android.view.SurfaceControl
-import androidx.test.filters.SmallTest
-import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.TestShellExecutor
-import com.android.wm.shell.sysui.ShellInit
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-
-/**
- * Tests for [PooledWindowDecorViewHostSupplier].
- *
- * Build/Install/Run:
- *  atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class PooledWindowDecorViewHostSupplierTest : ShellTestCase() {
-
-    private val testExecutor = TestShellExecutor()
-    private val testShellInit = ShellInit(testExecutor)
-    @Mock
-    private lateinit var mockViewHostFactory: ReusableWindowDecorViewHost.Factory
-
-    private lateinit var supplier: PooledWindowDecorViewHostSupplier
-
-    @Test
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun onInit_warmsAndPoolsViewHosts() = runTest {
-        supplier = createSupplier(maxPoolSize = 5, preWarmSize = 2)
-        val mockViewHost1 = mock<ReusableWindowDecorViewHost>()
-        val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
-        whenever(mockViewHostFactory
-            .create(context, this, context.display, id = 0))
-            .thenReturn(mockViewHost1)
-        whenever(mockViewHostFactory
-            .create(context, this, context.display, id = 1))
-            .thenReturn(mockViewHost2)
-
-        testExecutor.flushAll()
-        advanceUntilIdle()
-
-        // Both were warmed up.
-        verify(mockViewHost1).warmUp()
-        verify(mockViewHost2).warmUp()
-        // Both were released, so re-acquiring them provides the same instance.
-        assertThat(mockViewHost2)
-            .isEqualTo(supplier.acquire(context, context.display))
-        assertThat(mockViewHost1)
-            .isEqualTo(supplier.acquire(context, context.display))
-    }
-
-    @Test(expected = Throwable::class)
-    fun onInit_warmUpSizeExceedsPoolSize_throws() = runTest {
-        createSupplier(maxPoolSize = 3, preWarmSize = 4)
-    }
-
-    @Test
-    fun acquire_poolHasInstances_reuses() = runTest {
-        supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
-        // Prepare the pool with one instance.
-        val mockViewHost = mock<ReusableWindowDecorViewHost>()
-        supplier.release(mockViewHost, SurfaceControl.Transaction())
-
-        assertThat(mockViewHost)
-            .isEqualTo(supplier.acquire(context, context.display))
-        verify(mockViewHostFactory, never()).create(any(), any(), any(), any())
-    }
-
-    @Test
-    fun acquire_pooledHasZeroInstances_creates() = runTest {
-        supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
-        supplier.acquire(context, context.display)
-
-        verify(mockViewHostFactory).create(context, this, context.display, id = 0)
-    }
-
-    @Test
-    fun release_poolBelowLimit_caches() = runTest {
-        supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
-        val mockViewHost = mock<ReusableWindowDecorViewHost>()
-        val mockT = mock<SurfaceControl.Transaction>()
-        supplier.release(mockViewHost, mockT)
-
-        assertThat(mockViewHost)
-            .isEqualTo(supplier.acquire(context, context.display))
-    }
-
-    @Test
-    fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest {
-        supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
-        val mockViewHost = mock<ReusableWindowDecorViewHost>()
-        val mockT = mock<SurfaceControl.Transaction>()
-        supplier.release(mockViewHost, mockT)
-
-        verify(mockViewHost, never()).release(mockT)
-    }
-
-    @Test
-    fun release_poolAtLimit_doesNotCache() = runTest {
-        supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0)
-        val mockT = mock<SurfaceControl.Transaction>()
-        val mockViewHost = mock<ReusableWindowDecorViewHost>()
-        supplier.release(mockViewHost, mockT) // Maxes pool.
-
-        val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
-        supplier.release(mockViewHost2, mockT) // Beyond limit.
-
-        assertThat(mockViewHost)
-            .isEqualTo(supplier.acquire(context, context.display))
-        // Second one wasn't cached, so the acquired one should've been a new instance.
-        assertThat(mockViewHost2)
-            .isNotEqualTo(supplier.acquire(context, context.display))
-    }
-
-    @Test
-    fun release_poolAtLimit_releasesViewHost() = runTest {
-        supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0)
-        val mockT = mock<SurfaceControl.Transaction>()
-        val mockViewHost = mock<ReusableWindowDecorViewHost>()
-        supplier.release(mockViewHost, mockT) // Maxes pool.
-
-        val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
-        supplier.release(mockViewHost2, mockT) // Beyond limit.
-
-        // Second one doesn't fit, so it needs to be released.
-        verify(mockViewHost2).release(mockT)
-    }
-
-    private fun CoroutineScope.createSupplier(
-        maxPoolSize: Int,
-        preWarmSize: Int
-    ) = PooledWindowDecorViewHostSupplier(
-        context,
-        this,
-        testShellInit,
-        mockViewHostFactory,
-        maxPoolSize,
-        preWarmSize
-    ).also {
-        testShellInit.init()
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt
deleted file mode 100644
index de2444e..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt
+++ /dev/null
@@ -1,182 +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.wm.shell.windowdecor.viewhost
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.SurfaceControl
-import android.view.View
-import android.view.WindowManager
-import androidx.test.filters.SmallTest
-import com.android.wm.shell.ShellTestCase
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.kotlin.spy
-import org.mockito.kotlin.verify
-
-/**
- * Tests for [ReusableWindowDecorViewHost].
- *
- * Build/Install/Run:
- *  atest WMShellUnitTests:ReusableWindowDecorViewHostTest
- */
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner::class)
-class ReusableWindowDecorViewHostTest : ShellTestCase() {
-
-    @Test
-    fun warmUp_addsRootView() = runTest {
-        val reusableVH = createReusableViewHost().apply {
-            warmUp()
-        }
-
-        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView)
-    }
-
-    @Test
-    fun update_differentView_replacesView() = runTest {
-        val view = View(context)
-        val lp = WindowManager.LayoutParams()
-        val reusableVH = createReusableViewHost()
-        reusableVH.updateView(view, lp, context.resources.configuration, null)
-
-        assertThat(reusableVH.rootView.childCount).isEqualTo(1)
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
-
-        val newView = View(context)
-        val newLp = WindowManager.LayoutParams()
-        reusableVH.updateView(newView, newLp, context.resources.configuration, null)
-
-        assertThat(reusableVH.rootView.childCount).isEqualTo(1)
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    @Test
-    fun updateView_clearsPendingAsyncJob() = runTest {
-        val reusableVH = createReusableViewHost()
-        val asyncView = View(context)
-        val syncView = View(context)
-        val asyncAttrs = WindowManager.LayoutParams(100, 100)
-        val syncAttrs = WindowManager.LayoutParams(200, 200)
-
-        reusableVH.updateViewAsync(
-            view = asyncView,
-            attrs = asyncAttrs,
-            configuration = context.resources.configuration,
-        )
-
-        // No view host yet, since the coroutine hasn't run.
-        assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
-
-        reusableVH.updateView(
-            view = syncView,
-            attrs = syncAttrs,
-            configuration = context.resources.configuration,
-            onDrawTransaction = null
-        )
-
-        // Would run coroutine if it hadn't been cancelled.
-        advanceUntilIdle()
-
-        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
-        // View host view/attrs should match the ones from the sync call, plus, since the
-        // sync/async were made with different views, if the job hadn't been cancelled there
-        // would've been an exception thrown as replacing views isn't allowed.
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
-        assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    @Test
-    fun updateViewAsync() = runTest {
-        val reusableVH = createReusableViewHost()
-        val view = View(context)
-        val attrs = WindowManager.LayoutParams(100, 100)
-
-        reusableVH.updateViewAsync(
-            view = view,
-            attrs = attrs,
-            configuration = context.resources.configuration,
-        )
-
-        assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
-
-        advanceUntilIdle()
-
-        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    @Test
-    fun updateViewAsync_clearsPendingAsyncJob() = runTest {
-        val reusableVH = createReusableViewHost()
-
-        val view = View(context)
-        reusableVH.updateViewAsync(
-            view = view,
-            attrs = WindowManager.LayoutParams(100, 100),
-            configuration = context.resources.configuration,
-        )
-        val otherView = View(context)
-        reusableVH.updateViewAsync(
-            view = otherView,
-            attrs = WindowManager.LayoutParams(100, 100),
-            configuration = context.resources.configuration,
-        )
-
-        advanceUntilIdle()
-
-        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
-        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
-    }
-
-    @Test
-    fun release() = runTest {
-        val reusableVH = createReusableViewHost()
-
-        val view = View(context)
-        reusableVH.updateView(
-            view = view,
-            attrs = WindowManager.LayoutParams(100, 100),
-            configuration = context.resources.configuration,
-            onDrawTransaction = null
-        )
-
-        val t = mock(SurfaceControl.Transaction::class.java)
-        reusableVH.release(t)
-
-        verify(reusableVH.viewHostAdapter).release(t)
-    }
-
-    private fun CoroutineScope.createReusableViewHost() = ReusableWindowDecorViewHost(
-        context = context,
-        mainScope = this,
-        display = context.display,
-        id = 1,
-        viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
-    )
-
-    private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt
deleted file mode 100644
index d6c80a7..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt
+++ /dev/null
@@ -1,144 +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.wm.shell.windowdecor.viewhost
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.View
-import android.view.WindowManager
-import androidx.test.filters.SmallTest
-import com.android.wm.shell.ShellTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertThrows
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.kotlin.spy
-import org.mockito.kotlin.verify
-
-/**
- * Tests for [SurfaceControlViewHostAdapter].
- *
- * Build/Install/Run:
- * atest WMShellUnitTests:SurfaceControlViewHostAdapterTest
- */
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner::class)
-class SurfaceControlViewHostAdapterTest : ShellTestCase() {
-
-    private lateinit var adapter: SurfaceControlViewHostAdapter
-
-    @Before
-    fun setUp() {
-        adapter = SurfaceControlViewHostAdapter(
-            context,
-            context.display,
-            surfaceControlViewHostFactory = { c, d, wwm, s ->
-                spy(SurfaceControlViewHost(c, d, wwm, s))
-            }
-        )
-    }
-
-    @Test
-    fun prepareViewHost() {
-        adapter.prepareViewHost(context.resources.configuration)
-
-        assertThat(adapter.viewHost).isNotNull()
-    }
-
-    @Test
-    fun prepareViewHost_alreadyCreated_skips() {
-        adapter.prepareViewHost(context.resources.configuration)
-
-        val viewHost = adapter.viewHost!!
-
-        adapter.prepareViewHost(context.resources.configuration)
-
-        assertThat(adapter.viewHost).isEqualTo(viewHost)
-    }
-
-    @Test
-    fun updateView_layoutInViewHost() {
-        val view = View(context)
-        adapter.prepareViewHost(context.resources.configuration)
-
-        adapter.updateView(
-            view = view,
-            attrs = WindowManager.LayoutParams(100, 100)
-        )
-
-        assertThat(adapter.isInitialized()).isTrue()
-        assertThat(adapter.view()).isEqualTo(view)
-    }
-
-    @Test
-    fun updateView_alreadyLaidOut_relayouts() {
-        val view = View(context)
-        adapter.prepareViewHost(context.resources.configuration)
-        adapter.updateView(
-            view = view,
-            attrs = WindowManager.LayoutParams(100, 100)
-        )
-
-        val otherParams = WindowManager.LayoutParams(200, 200)
-        adapter.updateView(
-            view = view,
-            attrs = otherParams
-        )
-
-        assertThat(adapter.view()).isEqualTo(view)
-        assertThat(adapter.view()!!.layoutParams.width).isEqualTo(otherParams.width)
-    }
-
-    @Test
-    fun updateView_replacingView_throws() {
-        val view = View(context)
-        adapter.prepareViewHost(context.resources.configuration)
-        adapter.updateView(
-            view = view,
-            attrs = WindowManager.LayoutParams(100, 100)
-        )
-
-        val otherView = View(context)
-        assertThrows(Exception::class.java) {
-            adapter.updateView(
-                view = otherView,
-                attrs = WindowManager.LayoutParams(100, 100)
-            )
-        }
-    }
-
-    @Test
-    fun release() {
-        adapter.prepareViewHost(context.resources.configuration)
-        adapter.updateView(
-            view = View(context),
-            attrs = WindowManager.LayoutParams(100, 100)
-        )
-
-        val mockT = mock(SurfaceControl.Transaction::class.java)
-        adapter.release(mockT)
-
-        verify(adapter.viewHost!!).release()
-        verify(mockT).remove(adapter.rootSurface)
-    }
-
-    private fun SurfaceControlViewHostAdapter.view(): View? = viewHost?.view
-}
diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp
new file mode 100644
index 0000000..09e2f42
--- /dev/null
+++ b/libs/appfunctions/Android.bp
@@ -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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_sdk_library {
+    name: "com.google.android.appfunctions.sidecar",
+    owner: "google",
+    srcs: ["java/**/*.java"],
+    api_packages: ["com.google.android.appfunctions.sidecar"],
+    dex_preopt: {
+        enabled: false,
+    },
+    system_ext_specific: true,
+    no_dist: true,
+    unsafe_ignore_missing_latest_api: true,
+}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
new file mode 100644
index 0000000..504e329
--- /dev/null
+++ b/libs/appfunctions/api/current.txt
@@ -0,0 +1,49 @@
+// Signature format: 2.0
+package com.google.android.appfunctions.sidecar {
+
+  public final class AppFunctionManager {
+    ctor public AppFunctionManager(android.content.Context);
+    method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+  }
+
+  public abstract class AppFunctionService extends android.app.Service {
+    ctor public AppFunctionService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
+    field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+  }
+
+  public final class ExecuteAppFunctionRequest {
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public String getFunctionIdentifier();
+    method @NonNull public android.app.appsearch.GenericDocument getParameters();
+    method @NonNull public String getTargetPackageName();
+  }
+
+  public static final class ExecuteAppFunctionRequest.Builder {
+    ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String);
+    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build();
+    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
+    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
+  }
+
+  public final class ExecuteAppFunctionResponse {
+    method @Nullable public String getErrorMessage();
+    method @NonNull public android.os.Bundle getExtras();
+    method public int getResultCode();
+    method @NonNull public android.app.appsearch.GenericDocument getResultDocument();
+    method public boolean isSuccess();
+    method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle);
+    method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle);
+    field public static final String PROPERTY_RETURN_VALUE = "returnValue";
+    field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
+    field public static final int RESULT_DENIED = 1; // 0x1
+    field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
+    field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
+    field public static final int RESULT_OK = 0; // 0x0
+    field public static final int RESULT_TIMED_OUT = 5; // 0x5
+  }
+
+}
+
diff --git a/libs/appfunctions/api/removed.txt b/libs/appfunctions/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/system-current.txt b/libs/appfunctions/api/system-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/system-removed.txt b/libs/appfunctions/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/test-current.txt b/libs/appfunctions/api/test-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/test-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/test-removed.txt b/libs/appfunctions/api/test-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/test-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
new file mode 100644
index 0000000..b1dd467
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -0,0 +1,82 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+
+/**
+ * Provides app functions related functionalities.
+ *
+ * <p>App function is a specific piece of functionality that an app offers to the system. These
+ * functionalities can be integrated into various system features.
+ *
+ * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and
+ * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
+ */
+// TODO(b/357551503): Implement get and set enabled app function APIs.
+// TODO(b/367329899): Add sidecar library to Android B builds.
+public final class AppFunctionManager {
+    private final android.app.appfunctions.AppFunctionManager mManager;
+    private final Context mContext;
+
+    /**
+     * Creates an instance.
+     *
+     * @param context A {@link Context}.
+     * @throws java.lang.IllegalStateException if the underlying {@link
+     *   android.app.appfunctions.AppFunctionManager} is not found.
+     */
+    public AppFunctionManager(Context context) {
+        mContext = Objects.requireNonNull(context);
+        mManager = context.getSystemService(android.app.appfunctions.AppFunctionManager.class);
+        if (mManager == null) {
+            throw new IllegalStateException(
+                    "Underlying AppFunctionManager system service not found.");
+        }
+    }
+
+    /**
+     * Executes the app function.
+     *
+     * <p>Proxies request and response to the underlying {@link
+     * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
+     * response in the appropriate type required by the function.
+     */
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest sidecarRequest,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+        Objects.requireNonNull(sidecarRequest);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        android.app.appfunctions.ExecuteAppFunctionRequest platformRequest =
+                SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest);
+        mManager.executeAppFunction(
+                platformRequest, executor, (platformResponse) -> {
+                    callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse(
+                            platformResponse));
+                });
+    }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
new file mode 100644
index 0000000..65959df
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.appfunctions.sidecar;
+
+import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import java.util.function.Consumer;
+
+/**
+ * Abstract base class to provide app functions to the system.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourService"
+ *       android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
+ *    <intent-filter>
+ *      <action android:name="android.app.appfunctions.AppFunctionService" />
+ *    </intent-filter>
+ * </service>
+ * }
+ * </pre>
+ *
+ * <p>This class wraps {@link android.app.appfunctions.AppFunctionService} functionalities and
+ * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
+ *
+ * @see AppFunctionManager
+ */
+public abstract class AppFunctionService extends Service {
+    /**
+     * The permission to only allow system access to the functions through {@link
+     * AppFunctionManagerService}.
+     */
+    @NonNull
+    public static final String BIND_APP_FUNCTION_SERVICE =
+            "android.permission.BIND_APP_FUNCTION_SERVICE";
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service. To be supported, the
+     * service must also require the {@link BIND_APP_FUNCTION_SERVICE} permission so that other
+     * applications can not abuse it.
+     */
+    @NonNull
+    public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+
+    private final Binder mBinder =
+            android.app.appfunctions.AppFunctionService.createBinder(
+                    /* context= */ this,
+                    /* onExecuteFunction= */ (platformRequest, callback) -> {
+                        AppFunctionService.this.onExecuteFunction(
+                                SidecarConverter.getSidecarExecuteAppFunctionRequest(
+                                        platformRequest),
+                                (sidecarResponse) -> {
+                                    callback.accept(
+                                            SidecarConverter.getPlatformExecuteAppFunctionResponse(
+                                                    sidecarResponse));
+                                });
+                    }
+            );
+
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * @param request The function execution request.
+     * @param callback A callback to report back the result.
+     */
+    @MainThread
+    public abstract void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
new file mode 100644
index 0000000..fa6d2ff
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/**
+ * A request to execute an app function.
+ *
+ * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionRequest} without parcel
+ * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
+ * platform API).
+ */
+public final class ExecuteAppFunctionRequest {
+    /** Returns the package name of the app that hosts the function. */
+    @NonNull private final String mTargetPackageName;
+
+    /**
+     * Returns the unique string identifier of the app function to be executed. TODO(b/357551503):
+     * Document how callers can get the available function identifiers.
+     */
+    @NonNull private final String mFunctionIdentifier;
+
+    /** Returns additional metadata relevant to this function execution request. */
+    @NonNull private final Bundle mExtras;
+
+    /**
+     * Returns the parameters required to invoke this function. Within this [GenericDocument], the
+     * property names are the names of the function parameters and the property values are the
+     * values of those parameters.
+     *
+     * <p>The document may have missing parameters. Developers are advised to implement defensive
+     * handling measures.
+     *
+     * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution
+     */
+    @NonNull private final GenericDocument mParameters;
+
+    private ExecuteAppFunctionRequest(
+            @NonNull String targetPackageName,
+            @NonNull String functionIdentifier,
+            @NonNull Bundle extras,
+            @NonNull GenericDocument parameters) {
+        mTargetPackageName = Objects.requireNonNull(targetPackageName);
+        mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        mExtras = Objects.requireNonNull(extras);
+        mParameters = Objects.requireNonNull(parameters);
+    }
+
+    /** Returns the package name of the app that hosts the function. */
+    @NonNull
+    public String getTargetPackageName() {
+        return mTargetPackageName;
+    }
+
+    /** Returns the unique string identifier of the app function to be executed. */
+    @NonNull
+    public String getFunctionIdentifier() {
+        return mFunctionIdentifier;
+    }
+
+    /**
+     * Returns the function parameters. The key is the parameter name, and the value is the
+     * parameter value.
+     *
+     * <p>The bundle may have missing parameters. Developers are advised to implement defensive
+     * handling measures.
+     */
+    @NonNull
+    public GenericDocument getParameters() {
+        return mParameters;
+    }
+
+    /** Returns the additional data relevant to this function execution. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /** Builder for {@link ExecuteAppFunctionRequest}. */
+    public static final class Builder {
+        @NonNull private final String mTargetPackageName;
+        @NonNull private final String mFunctionIdentifier;
+        @NonNull private Bundle mExtras = Bundle.EMPTY;
+
+        @NonNull
+        private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();
+
+        public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) {
+            mTargetPackageName = Objects.requireNonNull(targetPackageName);
+            mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        }
+
+        /** Sets the additional data relevant to this function execution. */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = Objects.requireNonNull(extras);
+            return this;
+        }
+
+        /** Sets the function parameters. */
+        @NonNull
+        public Builder setParameters(@NonNull GenericDocument parameters) {
+            Objects.requireNonNull(parameters);
+            mParameters = parameters;
+            return this;
+        }
+
+        /** Builds the {@link ExecuteAppFunctionRequest}. */
+        @NonNull
+        public ExecuteAppFunctionRequest build() {
+            return new ExecuteAppFunctionRequest(
+                    mTargetPackageName,
+                    mFunctionIdentifier,
+                    mExtras,
+                    mParameters);
+        }
+    }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
new file mode 100644
index 0000000..60c25fa
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -0,0 +1,240 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The response to an app function execution.
+ *
+ * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel
+ * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
+ * platform API).
+ */
+public final class ExecuteAppFunctionResponse {
+    /**
+     * The name of the property that stores the function return value within the {@code
+     * resultDocument}.
+     *
+     * <p>See {@link GenericDocument#getProperty(String)} for more information.
+     *
+     * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will
+     * be empty {@link GenericDocument}.
+     *
+     * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will
+     * return {@code null}.
+     *
+     * <p>See {@link #getResultDocument} for more information on extracting the return value.
+     */
+    public static final String PROPERTY_RETURN_VALUE = "returnValue";
+
+    /** The call was successful. */
+    public static final int RESULT_OK = 0;
+
+    /** The caller does not have the permission to execute an app function. */
+    public static final int RESULT_DENIED = 1;
+
+    /** An unknown error occurred while processing the call in the AppFunctionService. */
+    public static final int RESULT_APP_UNKNOWN_ERROR = 2;
+
+    /**
+     * An internal error occurred within AppFunctionManagerService.
+     *
+     * <p>This error may be considered similar to {@link IllegalStateException}
+     */
+    public static final int RESULT_INTERNAL_ERROR = 3;
+
+    /**
+     * The caller supplied invalid arguments to the call.
+     *
+     * <p>This error may be considered similar to {@link IllegalArgumentException}.
+     */
+    public static final int RESULT_INVALID_ARGUMENT = 4;
+
+    /** The operation was timed out. */
+    public static final int RESULT_TIMED_OUT = 5;
+
+    /** The result code of the app function execution. */
+    @ResultCode private final int mResultCode;
+
+    /**
+     * The error message associated with the result, if any. This is {@code null} if the result code
+     * is {@link #RESULT_OK}.
+     */
+    @Nullable private final String mErrorMessage;
+
+    /**
+     * Returns the return value of the executed function.
+     *
+     * <p>The return value is stored in a {@link GenericDocument} with the key {@link
+     * #PROPERTY_RETURN_VALUE}.
+     *
+     * <p>See {@link #getResultDocument} for more information on extracting the return value.
+     */
+    @NonNull private final GenericDocument mResultDocument;
+
+    /** Returns the additional metadata data relevant to this function execution response. */
+    @NonNull private final Bundle mExtras;
+
+    private ExecuteAppFunctionResponse(
+            @NonNull GenericDocument resultDocument,
+            @NonNull Bundle extras,
+            @ResultCode int resultCode,
+            @Nullable String errorMessage) {
+        mResultDocument = Objects.requireNonNull(resultDocument);
+        mExtras = Objects.requireNonNull(extras);
+        mResultCode = resultCode;
+        mErrorMessage = errorMessage;
+    }
+
+    /**
+     * Returns result codes from throwable.
+     *
+     * @hide
+     */
+    static @ResultCode int getResultCode(@NonNull Throwable t) {
+        if (t instanceof IllegalArgumentException) {
+            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
+        }
+        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+    }
+
+    /**
+     * Returns a successful response.
+     *
+     * @param resultDocument The return value of the executed function.
+     * @param extras The additional metadata data relevant to this function execution response.
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse newSuccess(
+            @NonNull GenericDocument resultDocument, @Nullable Bundle extras) {
+        Objects.requireNonNull(resultDocument);
+        Bundle actualExtras = getActualExtras(extras);
+
+        return new ExecuteAppFunctionResponse(
+                resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null);
+    }
+
+    /**
+     * Returns a failure response.
+     *
+     * @param resultCode The result code of the app function execution.
+     * @param extras The additional metadata data relevant to this function execution response.
+     * @param errorMessage The error message associated with the result, if any.
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse newFailure(
+            @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) {
+        if (resultCode == RESULT_OK) {
+            throw new IllegalArgumentException("resultCode must not be RESULT_OK");
+        }
+        Bundle actualExtras = getActualExtras(extras);
+        GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build();
+        return new ExecuteAppFunctionResponse(
+                emptyDocument, actualExtras, resultCode, errorMessage);
+    }
+
+    private static Bundle getActualExtras(@Nullable Bundle extras) {
+        if (extras == null) {
+            return Bundle.EMPTY;
+        }
+        return extras;
+    }
+
+    /**
+     * Returns a generic document containing the return value of the executed function.
+     *
+     * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.
+     *
+     * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed
+     * function does not produce a return value.
+     *
+     * <p>Sample code for extracting the return value:
+     *
+     * <pre>
+     *     GenericDocument resultDocument = response.getResultDocument();
+     *     Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE);
+     *     if (returnValue != null) {
+     *       // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString},
+     *       // {@link GenericDocument#getPropertyLong} etc.
+     *       // Do something with the returnValue
+     *     }
+     * </pre>
+     */
+    @NonNull
+    public GenericDocument getResultDocument() {
+        return mResultDocument;
+    }
+
+    /** Returns the extras of the app function execution. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Returns {@code true} if {@link #getResultCode} equals {@link
+     * ExecuteAppFunctionResponse#RESULT_OK}.
+     */
+    public boolean isSuccess() {
+        return getResultCode() == RESULT_OK;
+    }
+
+    /**
+     * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /**
+     * Returns the error message associated with this result.
+     *
+     * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}.
+     */
+    @Nullable
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Result codes.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"RESULT_"},
+            value = {
+                    RESULT_OK,
+                    RESULT_DENIED,
+                    RESULT_APP_UNKNOWN_ERROR,
+                    RESULT_INTERNAL_ERROR,
+                    RESULT_INVALID_ARGUMENT,
+                    RESULT_TIMED_OUT,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultCode {}
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
new file mode 100644
index 0000000..b1b05f7
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
@@ -0,0 +1,104 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.NonNull;
+
+/**
+ * Utility class containing methods to convert Sidecar objects of AppFunctions API into the
+ * underlying platform classes.
+ *
+ * @hide
+ */
+public final class SidecarConverter {
+    private SidecarConverter() {}
+
+    /**
+     * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
+     * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.ExecuteAppFunctionRequest
+            getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) {
+        return new
+                android.app.appfunctions.ExecuteAppFunctionRequest.Builder(
+                request.getTargetPackageName(),
+                request.getFunctionIdentifier())
+                .setExtras(request.getExtras())
+                .setParameters(request.getParameters())
+                .build();
+    }
+
+    /**
+     * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
+     * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.ExecuteAppFunctionResponse
+            getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) {
+        if (response.isSuccess()) {
+            return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess(
+                    response.getResultDocument(), response.getExtras());
+        } else {
+            return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure(
+                    response.getResultCode(),
+                    response.getErrorMessage(),
+                    response.getExtras());
+        }
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
+     * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
+     *
+     * @hide
+     */
+    @NonNull
+    public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest(
+            @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) {
+        return new ExecuteAppFunctionRequest.Builder(
+                request.getTargetPackageName(),
+                request.getFunctionIdentifier())
+                .setExtras(request.getExtras())
+                .setParameters(request.getParameters())
+                .build();
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
+     * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
+     *
+     * @hide
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse(
+            @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) {
+        if (response.isSuccess()) {
+            return ExecuteAppFunctionResponse.newSuccess(
+                    response.getResultDocument(), response.getExtras());
+        } else {
+            return ExecuteAppFunctionResponse.newFailure(
+                    response.getResultCode(),
+                    response.getErrorMessage(),
+                    response.getExtras());
+        }
+    }
+}
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 1854361..84bd45d 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -264,6 +264,7 @@
         , mPixelStorageType(PixelStorageType::Heap) {
     mPixelStorage.heap.address = address;
     mPixelStorage.heap.size = size;
+    traceBitmapCreate();
 }
 
 Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info)
@@ -272,6 +273,7 @@
         , mPixelStorageType(PixelStorageType::WrappedPixelRef) {
     pixelRef.ref();
     mPixelStorage.wrapped.pixelRef = &pixelRef;
+    traceBitmapCreate();
 }
 
 Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes)
@@ -281,6 +283,7 @@
     mPixelStorage.ashmem.address = address;
     mPixelStorage.ashmem.fd = fd;
     mPixelStorage.ashmem.size = mappedSize;
+    traceBitmapCreate();
 }
 
 #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
@@ -297,10 +300,12 @@
     setImmutable();  // HW bitmaps are always immutable
     mImage = SkImages::DeferredFromAHardwareBuffer(buffer, mInfo.alphaType(),
                                                    mInfo.refColorSpace());
+    traceBitmapCreate();
 }
 #endif
 
 Bitmap::~Bitmap() {
+    traceBitmapDelete();
     switch (mPixelStorageType) {
         case PixelStorageType::WrappedPixelRef:
             mPixelStorage.wrapped.pixelRef->unref();
@@ -572,4 +577,28 @@
     mGainmap = std::move(gainmap);
 }
 
+std::mutex Bitmap::mLock{};
+size_t Bitmap::mTotalBitmapBytes = 0;
+size_t Bitmap::mTotalBitmapCount = 0;
+
+void Bitmap::traceBitmapCreate() {
+    if (ATRACE_ENABLED()) {
+        std::lock_guard lock{mLock};
+        mTotalBitmapBytes += getAllocationByteCount();
+        mTotalBitmapCount++;
+        ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes);
+        ATRACE_INT64("Bitmap Count", mTotalBitmapCount);
+    }
+}
+
+void Bitmap::traceBitmapDelete() {
+    if (ATRACE_ENABLED()) {
+        std::lock_guard lock{mLock};
+        mTotalBitmapBytes -= getAllocationByteCount();
+        mTotalBitmapCount--;
+        ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes);
+        ATRACE_INT64("Bitmap Count", mTotalBitmapCount);
+    }
+}
+
 }  // namespace android
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index dd344e2..3d55d85 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -25,6 +25,7 @@
 #include <cutils/compiler.h>
 #include <utils/StrongPointer.h>
 
+#include <mutex>
 #include <optional>
 
 #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
@@ -227,6 +228,13 @@
     } mPixelStorage;
 
     sk_sp<SkImage> mImage;  // Cache is used only for HW Bitmaps with Skia pipeline.
+
+    // for tracing total number and memory usage of bitmaps
+    static std::mutex mLock;
+    static size_t mTotalBitmapBytes;
+    static size_t mTotalBitmapCount;
+    void traceBitmapCreate();
+    void traceBitmapDelete();
 };
 
 }  // namespace android
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index d7bf201..e13e1365 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -73,6 +73,7 @@
     }
     paint->setStrokeJoin(SkPaint::kRound_Join);
     paint->setLooper(nullptr);
+    paint->setBlendMode(SkBlendMode::kSrcOver);
 }
 
 class DrawTextFunctor {
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/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index cddc337..c3cb492 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -94,6 +94,16 @@
 }
 
 flag {
+    name: "use_legacy_ntp_time"
+    namespace: "location"
+    description: "Flag for switching to legacy NtpNetworkTimeHelper"
+    bug: "368034558"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "subscriptions_changed_listener_thread"
     namespace: "location"
     description: "Flag for running onSubscriptionsChangedListener on FgThread"
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index bc8a7af..9603c0a 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -60,8 +60,13 @@
     method @FlaggedApi("android.nfc.nfc_oem_extension") @NonNull public java.util.List<java.lang.String> getActiveNfceeList();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
+    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOn(int);
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull android.nfc.NfcOemExtension.Callback);
+    field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int DISABLE = 0; // 0x0
+    field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int ENABLE_DEFAULT = 1; // 0x1
+    field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int ENABLE_EE = 3; // 0x3
+    field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int ENABLE_TRANSPARENT = 2; // 0x2
     field public static final int HCE_ACTIVATE = 1; // 0x1
     field public static final int HCE_DATA_TRANSFERRED = 2; // 0x2
     field public static final int HCE_DEACTIVATE = 3; // 0x3
diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl
index e2ec952..246efc7 100644
--- a/nfc/java/android/nfc/INfcAdapter.aidl
+++ b/nfc/java/android/nfc/INfcAdapter.aidl
@@ -73,7 +73,7 @@
     boolean setNfcSecure(boolean enable);
     NfcAntennaInfo getNfcAntennaInfo();
 
-    boolean setControllerAlwaysOn(boolean value);
+    void setControllerAlwaysOn(int mode);
     boolean isControllerAlwaysOn();
     boolean isControllerAlwaysOnSupported();
     void registerControllerAlwaysOnListener(in INfcControllerAlwaysOnListener listener);
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index f478793..de85f1e 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -559,6 +559,18 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface TagIntentAppPreferenceResult {}
 
+    /**
+     * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+     * @hide
+     */
+    public static final int CONTROLLER_ALWAYS_ON_MODE_DEFAULT = 1;
+
+    /**
+     * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+     * @hide
+     */
+    public static final int CONTROLLER_ALWAYS_ON_DISABLE = 0;
+
     // Guarded by sLock
     static boolean sIsInitialized = false;
     static boolean sHasNfcFeature;
@@ -2330,7 +2342,8 @@
      * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
      * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
      * are unavailable
-     * @return void
+     * @return true if feature is supported by the device and operation has bee initiated,
+     * false if the feature is not supported by the device.
      * @hide
      */
     @SystemApi
@@ -2339,8 +2352,13 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        return callServiceReturn(() ->  sService.setControllerAlwaysOn(value), false);
-
+        int mode = value ? CONTROLLER_ALWAYS_ON_MODE_DEFAULT : CONTROLLER_ALWAYS_ON_DISABLE;
+        try {
+            callService(() -> sService.setControllerAlwaysOn(mode));
+        } catch (UnsupportedOperationException e) {
+            return false;
+        }
+        return true;
     }
 
     /**
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index d51b704..45038d4 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -70,6 +70,58 @@
     private boolean mRfDiscoveryStarted = false;
 
     /**
+     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Enables the controller in default mode when NFC is disabled (existing API behavior).
+     * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    public static final int ENABLE_DEFAULT = NfcAdapter.CONTROLLER_ALWAYS_ON_MODE_DEFAULT;
+
+    /**
+     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Enables the controller in transparent mode when NFC is disabled.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    public static final int ENABLE_TRANSPARENT = 2;
+
+    /**
+     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Enables the controller and initializes and enables the EE subsystem when NFC is disabled.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    public static final int ENABLE_EE = 3;
+
+    /**
+     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Disable the Controller Always On Mode.
+     * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    public static final int DISABLE = NfcAdapter.CONTROLLER_ALWAYS_ON_DISABLE;
+
+    /**
+     * Possible controller modes for {@link #setControllerAlwaysOn(int)}.
+     *
+     * @hide
+     */
+    @IntDef(prefix = { "" }, value = {
+        ENABLE_DEFAULT,
+        ENABLE_TRANSPARENT,
+        ENABLE_EE,
+        DISABLE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ControllerMode{}
+
+    /**
      * Event that Host Card Emulation is activated.
      */
     public static final int HCE_ACTIVATE = 1;
@@ -389,6 +441,32 @@
             NfcAdapter.sService.fetchActiveNfceeList(), new ArrayList<String>());
     }
 
+    /**
+     * Sets NFC controller always on feature.
+     * <p>This API is for the NFCC internal state management. It allows to discriminate
+     * the controller function from the NFC function by keeping the NFC controller on without
+     * any NFC RF enabled if necessary.
+     * <p>This call is asynchronous, register listener {@link NfcAdapter.ControllerAlwaysOnListener}
+     * by {@link NfcAdapter#registerControllerAlwaysOnListener} to find out when the operation is
+     * complete.
+     * @param mode one of {@link ControllerMode} modes
+     * @throws UnsupportedOperationException if
+     *   <li> if FEATURE_NFC, FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
+     *   FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
+     *   are unavailable </li>
+     *   <li> if the feature is unavailable @see NfcAdapter#isNfcControllerAlwaysOnSupported() </li>
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
+    public void setControllerAlwaysOn(@ControllerMode int mode) {
+        if (!NfcAdapter.sHasNfcFeature && !NfcAdapter.sHasCeFeature) {
+            throw new UnsupportedOperationException();
+        }
+        NfcAdapter.callService(() -> NfcAdapter.sService.setControllerAlwaysOn(mode));
+    }
+
     private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {
 
         @Override
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index bd84b58..a30c0c3 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 package {
+    default_team: "trendy_team_framework_android_packages",
     default_applicable_licenses: [
         "frameworks_base_packages_PackageInstaller_license",
     ],
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index 717ec02..91882fd 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -65,6 +65,17 @@
       "name": "CtsIntentSignatureTestCases"
     },
     {
+      "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+      "options":[
+        {
+          "exclude-annotation":"androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation":"org.junit.Ignore"
+        }
+      ]
+    },
+    {
       "name": "CtsPackageInstallerCUJInstallationTestCases",
       "options":[
         {
@@ -76,6 +87,17 @@
       ]
     },
     {
+      "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+      "options":[
+        {
+          "exclude-annotation":"androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation":"org.junit.Ignore"
+        }
+      ]
+    },
+    {
       "name": "CtsPackageInstallerCUJUninstallationTestCases",
       "options":[
         {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index 022dded..265864e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -17,11 +17,16 @@
 package com.android.settingslib.spa.widget.dialog
 
 import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
@@ -32,9 +37,11 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.DialogProperties
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
 
 data class AlertDialogButton(
     val text: String,
@@ -85,27 +92,41 @@
     AlertDialog(
         onDismissRequest = ::close,
         modifier = Modifier.width(getDialogWidth()),
-        confirmButton = { confirmButton?.let { Button(it) } },
-        dismissButton = dismissButton?.let { { Button(it) } },
-        title = title?.let { { Text(it) } },
-        text = text?.let {
-            {
-                Column(Modifier.verticalScroll(rememberScrollState())) {
-                    text()
-                }
-            }
+        confirmButton = {
+            confirmButton?.let { if (isSpaExpressiveEnabled) ConfirmButton(it) else Button(it) }
         },
+        dismissButton =
+            dismissButton?.let {
+                { if (isSpaExpressiveEnabled) DismissButton(it) else Button(it) }
+            },
+        title = title?.let { { CenterRow { Text(it) } } },
+        text =
+            text?.let {
+                { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
+            },
         properties = DialogProperties(usePlatformDefaultWidth = false),
     )
 }
 
 @Composable
+internal fun CenterRow(content: @Composable (() -> Unit)) {
+    if (isSpaExpressiveEnabled) {
+        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+            content()
+        }
+    } else {
+        content()
+    }
+}
+
+@Composable
 fun getDialogWidth(): Dp {
     val configuration = LocalConfiguration.current
-    return configuration.screenWidthDp.dp * when (configuration.orientation) {
-        Configuration.ORIENTATION_LANDSCAPE -> 0.65f
-        else -> 0.85f
-    }
+    return configuration.screenWidthDp.dp *
+        when (configuration.orientation) {
+            Configuration.ORIENTATION_LANDSCAPE -> 0.65f
+            else -> 0.85f
+        }
 }
 
 @Composable
@@ -120,3 +141,47 @@
         Text(button.text)
     }
 }
+
+@Composable
+private fun AlertDialogPresenter.DismissButton(button: AlertDialogButton) {
+    OutlinedButton(
+        onClick = {
+            close()
+            button.onClick()
+        },
+        enabled = button.enabled,
+    ) {
+        Text(button.text)
+    }
+}
+
+@Composable
+private fun AlertDialogPresenter.ConfirmButton(button: AlertDialogButton) {
+    Button(
+        onClick = {
+            close()
+            button.onClick()
+        },
+        enabled = button.enabled,
+    ) {
+        Text(button.text)
+    }
+}
+
+@Preview
+@Composable
+private fun AlertDialogPreview() {
+    val alertDialogPresenter = remember {
+        object : AlertDialogPresenter {
+            override fun open() {}
+
+            override fun close() {}
+        }
+    }
+    alertDialogPresenter.SettingsAlertDialog(
+        confirmButton = AlertDialogButton("Ok"),
+        dismissButton = AlertDialogButton("Cancel"),
+        title = "Title",
+        text = { Text("Text") },
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
index 030522d..58a83fa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
@@ -40,10 +40,7 @@
     dismissButton: AlertDialogButton?,
     title: String?,
     icon: @Composable (() -> Unit)? = {
-        Icon(
-            Icons.Default.WarningAmber,
-            contentDescription = null
-        )
+        Icon(Icons.Default.WarningAmber, contentDescription = null)
     },
     text: @Composable (() -> Unit)?,
 ) {
@@ -52,43 +49,22 @@
         icon = icon,
         modifier = Modifier.width(getDialogWidth()),
         confirmButton = {
-            confirmButton?.let {
-                Button(
-                    onClick = {
-                        it.onClick()
-                    },
-                ) {
-                    Text(it.text)
+            confirmButton?.let { Button(onClick = { it.onClick() }) { Text(it.text) } }
+        },
+        dismissButton =
+            dismissButton?.let { { OutlinedButton(onClick = { it.onClick() }) { Text(it.text) } } },
+        title =
+            title?.let {
+                {
+                    CenterRow {
+                        Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
+                    }
                 }
-            }
-        },
-        dismissButton = dismissButton?.let {
-            {
-                OutlinedButton(
-                    onClick = {
-                        it.onClick()
-                    },
-                ) {
-                    Text(it.text)
-                }
-            }
-        },
-        title = title?.let {
-            {
-                Text(
-                    it,
-                    modifier = Modifier.fillMaxWidth(),
-                    textAlign = TextAlign.Center
-                )
-            }
-        },
-        text = text?.let {
-            {
-                Column(Modifier.verticalScroll(rememberScrollState())) {
-                    text()
-                }
-            }
-        },
+            },
+        text =
+            text?.let {
+                { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
+            },
         properties = DialogProperties(usePlatformDefaultWidth = false),
     )
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
index bef0bca..9f2210d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
@@ -58,10 +58,7 @@
     dismissButton: AlertDialogButton?,
     title: String?,
     icon: @Composable (() -> Unit)? = {
-        Icon(
-            Icons.Default.WarningAmber,
-            contentDescription = null
-        )
+        Icon(Icons.Default.WarningAmber, contentDescription = null)
     },
     text: @Composable (() -> Unit)?,
 ) {
@@ -69,42 +66,22 @@
         buttons = {
             AlertDialogFlowRow(
                 mainAxisSpacing = ButtonsMainAxisSpacing,
-                crossAxisSpacing = ButtonsCrossAxisSpacing
+                crossAxisSpacing = ButtonsCrossAxisSpacing,
             ) {
-                dismissButton?.let {
-                    OutlinedButton(onClick = it.onClick) {
-                        Text(it.text)
-                    }
-                }
-                confirmButton?.let {
-                    Button(
-                        onClick = {
-                            it.onClick()
-                        },
-                    ) {
-                        Text(it.text)
-                    }
-                }
+                dismissButton?.let { OutlinedButton(onClick = it.onClick) { Text(it.text) } }
+                confirmButton?.let { Button(onClick = { it.onClick() }) { Text(it.text) } }
             }
         },
         icon = icon,
         modifier = Modifier.width(getDialogWidth()),
-        title = title?.let {
-            {
-                Text(
-                    it,
-                    modifier = Modifier.fillMaxWidth(),
-                    textAlign = TextAlign.Center
-                )
-            }
-        },
-        text = text?.let {
-            {
-                Column(Modifier.verticalScroll(rememberScrollState())) {
-                    text()
-                }
-            }
-        },
+        title =
+            title?.let {
+                { Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) }
+            },
+        text =
+            text?.let {
+                { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
+            },
     )
 }
 
@@ -121,18 +98,12 @@
         shape = SettingsShape.CornerExtraLarge,
         color = MaterialTheme.colorScheme.surfaceContainerHigh,
     ) {
-        Column(
-            modifier = Modifier.padding(DialogPadding)
-        ) {
+        Column(modifier = Modifier.padding(DialogPadding)) {
             icon?.let {
                 CompositionLocalProvider(
-                    LocalContentColor provides AlertDialogDefaults.iconContentColor,
+                    LocalContentColor provides AlertDialogDefaults.iconContentColor
                 ) {
-                    Box(
-                        Modifier
-                            .padding(IconPadding)
-                            .align(Alignment.CenterHorizontally)
-                    ) {
+                    Box(Modifier.padding(IconPadding).align(Alignment.CenterHorizontally)) {
                         icon()
                     }
                 }
@@ -144,8 +115,7 @@
                 ) {
                     Box(
                         // Align the title to the center when an icon is present.
-                        Modifier
-                            .padding(TitlePadding)
+                        Modifier.padding(TitlePadding)
                             .align(
                                 if (icon == null) {
                                     Alignment.Start
@@ -161,11 +131,10 @@
             text?.let {
                 ProvideContentColorTextStyle(
                     contentColor = AlertDialogDefaults.textContentColor,
-                    textStyle = MaterialTheme.typography.bodyMedium
+                    textStyle = MaterialTheme.typography.bodyMedium,
                 ) {
                     Box(
-                        Modifier
-                            .weight(weight = 1f, fill = false)
+                        Modifier.weight(weight = 1f, fill = false)
                             .padding(TextPadding)
                             .align(Alignment.Start)
                     ) {
@@ -177,7 +146,7 @@
                 ProvideContentColorTextStyle(
                     contentColor = MaterialTheme.colorScheme.primary,
                     textStyle = MaterialTheme.typography.labelLarge,
-                    content = buttons
+                    content = buttons,
                 )
             }
         }
@@ -188,7 +157,7 @@
 internal fun AlertDialogFlowRow(
     mainAxisSpacing: Dp,
     crossAxisSpacing: Dp,
-    content: @Composable () -> Unit
+    content: @Composable () -> Unit,
 ) {
     Layout(content) { measurables, constraints ->
         val sequences = mutableListOf<List<Placeable>>()
@@ -204,8 +173,9 @@
 
         // Return whether the placeable can be added to the current sequence.
         fun canAddToCurrentSequence(placeable: Placeable) =
-            currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() +
-                placeable.width <= constraints.maxWidth
+            currentSequence.isEmpty() ||
+                currentMainAxisSize + mainAxisSpacing.roundToPx() + placeable.width <=
+                    constraints.maxWidth
 
         // Store current sequence information and start a new sequence.
         fun startNewSequence() {
@@ -213,8 +183,7 @@
                 crossAxisSpace += crossAxisSpacing.roundToPx()
             }
             // Ensures that confirming actions appear above dismissive actions.
-            @Suppress("ListIterator")
-            sequences.add(0, currentSequence.toList())
+            @Suppress("ListIterator") sequences.add(0, currentSequence.toList())
             crossAxisSizes += currentCrossAxisSize
             crossAxisPositions += crossAxisSpace
 
@@ -254,23 +223,23 @@
 
         layout(layoutWidth, layoutHeight) {
             sequences.fastForEachIndexed { i, placeables ->
-                val childrenMainAxisSizes = IntArray(placeables.size) { j ->
-                    placeables[j].width +
-                        if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
-                }
+                val childrenMainAxisSizes =
+                    IntArray(placeables.size) { j ->
+                        placeables[j].width +
+                            if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
+                    }
                 val arrangement = Arrangement.End
                 val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
                 with(arrangement) {
                     arrange(
-                        mainAxisLayoutSize, childrenMainAxisSizes,
-                        layoutDirection, mainAxisPositions
+                        mainAxisLayoutSize,
+                        childrenMainAxisSizes,
+                        layoutDirection,
+                        mainAxisPositions,
                     )
                 }
                 placeables.fastForEachIndexed { j, placeable ->
-                    placeable.place(
-                        x = mainAxisPositions[j],
-                        y = crossAxisPositions[i]
-                    )
+                    placeable.place(x = mainAxisPositions[j], y = crossAxisPositions[i])
                 }
             }
         }
@@ -289,8 +258,8 @@
 /**
  * ProvideContentColorTextStyle
  *
- * A convenience method to provide values to both LocalContentColor and LocalTextStyle in
- * one call. This is less expensive than nesting calls to CompositionLocalProvider.
+ * A convenience method to provide values to both LocalContentColor and LocalTextStyle in one call.
+ * This is less expensive than nesting calls to CompositionLocalProvider.
  *
  * Text styles will be merged with the current value of LocalTextStyle.
  */
@@ -298,12 +267,12 @@
 private fun ProvideContentColorTextStyle(
     contentColor: Color,
     textStyle: TextStyle,
-    content: @Composable () -> Unit
+    content: @Composable () -> Unit,
 ) {
     val mergedStyle = LocalTextStyle.current.merge(textStyle)
     CompositionLocalProvider(
         LocalContentColor provides contentColor,
         LocalTextStyle provides mergedStyle,
-        content = content
+        content = content,
     )
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 6f2567b..a3f9e51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -80,7 +80,9 @@
             "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE";
     public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE";
     public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE";
+    public static final String EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE = "BT_DEVICE_TO_AUTO_ADD_SOURCE";
     public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING";
+    public static final String EXTRA_PAIR_AND_JOIN_SHARING = "PAIR_AND_JOIN_SHARING";
     public static final int BROADCAST_STATE_UNKNOWN = 0;
     public static final int BROADCAST_STATE_ON = 1;
     public static final int BROADCAST_STATE_OFF = 2;
@@ -1224,7 +1226,7 @@
         Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
         intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state);
         intent.setPackage(mContext.getPackageName());
-        Log.e(TAG, "notifyBroadcastStateChange for state = " + state);
+        Log.d(TAG, "notifyBroadcastStateChange for state = " + state);
         mContext.sendBroadcast(intent);
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
index 01bb6f0..7ee7180 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
@@ -33,6 +33,7 @@
     private final String mTitle;
     private final ImmutableList<ToggleInfo> mToggleInfos;
     private final int mState;
+    private final boolean mIsActive;
     private final boolean mIsAllowedChangingState;
     private final Bundle mExtras;
 
@@ -40,6 +41,7 @@
             @NonNull String title,
             List<ToggleInfo> toggleInfos,
             int state,
+            boolean isActive,
             boolean allowChangingState,
             Bundle extras) {
         super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
@@ -47,6 +49,7 @@
         mTitle = title;
         mToggleInfos = ImmutableList.copyOf(toggleInfos);
         mState = state;
+        mIsActive = isActive;
         mIsAllowedChangingState = allowChangingState;
         mExtras = extras;
     }
@@ -67,9 +70,11 @@
         List<ToggleInfo> toggleInfos = new ArrayList<>();
         in.readTypedList(toggleInfos, ToggleInfo.CREATOR);
         int state = in.readInt();
+        boolean isActive = in.readBoolean();
         boolean allowChangingState = in.readBoolean();
         Bundle extras = in.readBundle(Bundle.class.getClassLoader());
-        return new MultiTogglePreference(title, toggleInfos, state, allowChangingState, extras);
+        return new MultiTogglePreference(
+                title, toggleInfos, state, isActive, allowChangingState, extras);
     }
 
     public static final Creator<MultiTogglePreference> CREATOR =
@@ -99,6 +104,7 @@
         dest.writeString(mTitle);
         dest.writeTypedList(mToggleInfos, flags);
         dest.writeInt(mState);
+        dest.writeBoolean(mIsActive);
         dest.writeBoolean(mIsAllowedChangingState);
         dest.writeBundle(mExtras);
     }
@@ -108,6 +114,7 @@
         private String mTitle;
         private ImmutableList.Builder<ToggleInfo> mToggleInfos = new ImmutableList.Builder<>();
         private int mState;
+        private boolean mIsActive;
         private boolean mAllowChangingState;
         private Bundle mExtras = Bundle.EMPTY;
 
@@ -148,6 +155,19 @@
         }
 
         /**
+         * Sets whether the current state is considered as an "active" state. If it's set to true,
+         * the toggle will be highlighted in UI.
+         *
+         * @param isActive The active state.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setIsActive(boolean isActive) {
+            mIsActive = isActive;
+            return this;
+        }
+
+        /**
          * Sets whether state can be changed by user.
          *
          * @param allowChangingState Whether user is allowed to change state.
@@ -178,7 +198,7 @@
         @NonNull
         public MultiTogglePreference build() {
             return new MultiTogglePreference(
-                    mTitle, mToggleInfos.build(), mState, mAllowChangingState, mExtras);
+                    mTitle, mToggleInfos.build(), mState, mIsActive, mAllowChangingState, mExtras);
         }
     }
 
@@ -202,6 +222,16 @@
     }
 
     /**
+     * Whether the current state is considered as an active state. If it's set to true, the toggle
+     * will be highlighted in UI.
+     *
+     * @return Returns the active state.
+     */
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    /**
      * Gets the toggle list in the preference.
      *
      * @return the toggle list.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index 29664f6..851b614 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -159,7 +159,7 @@
                     title = pref.title,
                     toggles = pref.toggleInfos.map { it.toModel() },
                     isAllowedChangingState = pref.isAllowedChangingState,
-                    isActive = true,
+                    isActive = pref.isActive,
                     state = DeviceSettingStateModel.MultiTogglePreferenceState(pref.state),
                     updateState = { newState ->
                         coroutineScope.launch(backgroundCoroutineContext) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 7eae5b2..3d8ff86 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -56,11 +56,13 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
 import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -101,8 +103,7 @@
                     } else if (allStatus.all { it is ServiceConnectionStatus.Connected }) {
                         allStatus
                             .filterIsInstance<
-                                ServiceConnectionStatus.Connected<
-                                        IDeviceSettingsProviderService>
+                                ServiceConnectionStatus.Connected<IDeviceSettingsProviderService>
                             >()
                             .all { it.service.serviceStatus?.enabled == true }
                     } else {
@@ -232,7 +233,7 @@
                                 IDeviceSettingsProviderService.Stub::asInterface,
                             )
                             .stateIn(
-                                coroutineScope,
+                                coroutineScope.plus(backgroundCoroutineContext),
                                 SharingStarted.WhileSubscribed(),
                                 ServiceConnectionStatus.Connecting,
                             )
@@ -263,21 +264,30 @@
         transform: ((IBinder) -> T),
     ): Flow<ServiceConnectionStatus<T>> {
         return callbackFlow {
-            val serviceConnection =
-                object : ServiceConnection {
-                    override fun onServiceConnected(name: ComponentName, service: IBinder) {
-                        launch { send(ServiceConnectionStatus.Connected(transform(service))) }
-                    }
+                val serviceConnection =
+                    object : ServiceConnection {
+                        override fun onServiceConnected(name: ComponentName, service: IBinder) {
+                            launch { send(ServiceConnectionStatus.Connected(transform(service))) }
+                        }
 
-                    override fun onServiceDisconnected(name: ComponentName?) {
-                        launch { send(ServiceConnectionStatus.Connecting) }
+                        override fun onServiceDisconnected(name: ComponentName?) {
+                            launch { send(ServiceConnectionStatus.Connecting) }
+                        }
                     }
+                if (
+                    !context.bindService(
+                        intent,
+                        Context.BIND_AUTO_CREATE,
+                        { launch { it.run() } },
+                        serviceConnection,
+                    )
+                ) {
+                    Log.w(TAG, "Fail to bind service $intent")
+                    launch { send(ServiceConnectionStatus.Failed) }
                 }
-            if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) {
-                launch { send(ServiceConnectionStatus.Failed) }
+                awaitClose { context.unbindService(serviceConnection) }
             }
-            awaitClose { context.unbindService(serviceConnection) }
-        }
+            .flowOn(backgroundCoroutineContext)
     }
 
     private suspend fun tryGetEndpointFromMetadata(cachedDevice: CachedBluetoothDevice): EndPoint? =
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index 766cd43..9dd2dbb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -80,6 +80,10 @@
                 context, id, audioDeviceInfoType, maxVolume, currentVolume, isVolumeFixed);
     }
 
+    public @AudioDeviceType int getAudioDeviceInfoType() {
+        return mAudioDeviceInfoType;
+    }
+
     public static boolean isSupportedInputDevice(@AudioDeviceType int audioDeviceInfoType) {
         return switch (audioDeviceInfoType) {
             case TYPE_BUILTIN_MIC,
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 874e030..0c50166 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -15,13 +15,20 @@
  */
 package com.android.settingslib.media;
 
+import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
+
 import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.MediaRecorder;
 import android.os.Handler;
+import android.util.Slog;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -35,12 +42,18 @@
 
     private static final String TAG = "InputRouteManager";
 
+    @VisibleForTesting
+    static final AudioAttributes INPUT_ATTRIBUTES =
+            new AudioAttributes.Builder().setCapturePreset(MediaRecorder.AudioSource.MIC).build();
+
     private final Context mContext;
 
     private final AudioManager mAudioManager;
 
     @VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
 
+    private MediaDevice mSelectedInputDevice;
+
     private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
 
     @VisibleForTesting
@@ -76,8 +89,27 @@
         mCallbacks.remove(callback);
     }
 
+    public @Nullable MediaDevice getSelectedInputDevice() {
+        return mSelectedInputDevice;
+    }
+
     private void dispatchInputDeviceListUpdate() {
-        // TODO (b/360175574): Get selected input device.
+        // Get selected input device.
+        List<AudioDeviceAttributes> attributesOfSelectedInputDevices =
+                mAudioManager.getDevicesForAttributes(INPUT_ATTRIBUTES);
+        int selectedInputDeviceAttributesType;
+        if (attributesOfSelectedInputDevices.isEmpty()) {
+            Slog.e(TAG, "Unexpected empty list of input devices. Using built-in mic.");
+            selectedInputDeviceAttributesType = AudioDeviceInfo.TYPE_BUILTIN_MIC;
+        } else {
+            if (attributesOfSelectedInputDevices.size() > 1) {
+                Slog.w(
+                        TAG,
+                        "AudioManager.getDevicesForAttributes returned more than one element."
+                                + " Using the first one.");
+            }
+            selectedInputDeviceAttributesType = attributesOfSelectedInputDevices.get(0).getType();
+        }
 
         // Get all input devices.
         AudioDeviceInfo[] audioDeviceInfos =
@@ -93,6 +125,10 @@
                             getCurrentInputGain(),
                             isInputGainFixed());
             if (mediaDevice != null) {
+                if (info.getType() == selectedInputDeviceAttributesType) {
+                    mediaDevice.setState(STATE_SELECTED);
+                    mSelectedInputDevice = mediaDevice;
+                }
                 mInputMediaDevices.add(mediaDevice);
             }
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
index 251cd36..9a9a960 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
@@ -87,7 +87,7 @@
      * Returns the conversation info drawable
      */
     public Drawable getBaseIconDrawable(ShortcutInfo shortcutInfo) {
-        return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
+        return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFullResIconDpi);
     }
 
     /**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
index 62fcb5e..1c7b5bc 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
@@ -120,6 +120,7 @@
                         .addToggleInfo(TOGGLE_INFO_1)
                         .addToggleInfo(TOGGLE_INFO_2)
                         .setState(123)
+                        .setIsActive(true)
                         .setAllowChangingState(true)
                         .setExtras(buildBundle("key1", "value1"))
                         .build();
@@ -130,6 +131,7 @@
         assertThat(fromParcel.getToggleInfos().stream().map(ToggleInfo::getLabel).toList())
                 .containsExactly("label1", "label2");
         assertThat(fromParcel.getState()).isEqualTo(preference.getState());
+        assertThat(fromParcel.isActive()).isEqualTo(preference.isActive());
         assertThat(fromParcel.isAllowedChangingState())
                 .isEqualTo(preference.isAllowedChangingState());
         assertThat(fromParcel.getExtras().getString("key1"))
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 81b5634..0cb6bc1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -102,9 +102,9 @@
         `when`(settingProviderService2.queryLocalInterface(anyString()))
             .thenReturn(settingProviderService2)
 
-        `when`(context.bindService(any(), any(), anyInt())).then { input ->
+        `when`(context.bindService(any(), anyInt(), any(), any())).then { input ->
             val intent = input.getArgument<Intent?>(0)
-            val connection = input.getArgument<ServiceConnection>(1)
+            val connection = input.getArgument<ServiceConnection>(3)
 
             when (intent?.action) {
                 CONFIG_SERVICE_INTENT_ACTION ->
@@ -210,7 +210,7 @@
     fun getDeviceSettingsConfig_bindingServiceFail_returnNull() {
         testScope.runTest {
             `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
-            doReturn(false).`when`(context).bindService(any(), any(), anyInt())
+            doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any())
 
             val config = underTest.getDeviceSettingsConfig(cachedDevice)
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index 2501ae6..8a18d07 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.media;
 
+import static com.android.settingslib.media.InputRouteManager.INPUT_ATTRIBUTES;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
@@ -23,6 +25,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 
@@ -36,6 +39,10 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {ShadowRouter2Manager.class})
 public class InputRouteManagerTest {
@@ -124,6 +131,97 @@
     }
 
     @Test
+    public void getSelectedInputDevice_returnOneFromAudioManager() {
+        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+        when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+
+        final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceInfo[] devices = {info1, info2};
+        when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+        // Mock audioManager.getDevicesForAttributes returns exactly one audioDeviceAttributes.
+        AudioDeviceAttributes audioDeviceAttributes = new AudioDeviceAttributes(info1);
+        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+                .thenReturn(Collections.singletonList(audioDeviceAttributes));
+
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // The selected input device has the same type as the one returned from AudioManager.
+        InputMediaDevice selectedInputDevice =
+                (InputMediaDevice) inputRouteManager.getSelectedInputDevice();
+        assertThat(selectedInputDevice.getAudioDeviceInfoType())
+                .isEqualTo(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+    }
+
+    @Test
+    public void getSelectedInputDevice_returnMoreThanOneFromAudioManager() {
+        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+        when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+
+        final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceInfo[] devices = {info1, info2};
+        when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+        // Mock audioManager.getDevicesForAttributes returns more than one audioDeviceAttributes.
+        AudioDeviceAttributes audioDeviceAttributes1 = new AudioDeviceAttributes(info1);
+        AudioDeviceAttributes audioDeviceAttributes2 = new AudioDeviceAttributes(info2);
+        List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+        attributesOfSelectedInputDevices.add(audioDeviceAttributes1);
+        attributesOfSelectedInputDevices.add(audioDeviceAttributes2);
+        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+                .thenReturn(attributesOfSelectedInputDevices);
+
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // The selected input device has the same type as the first one returned from AudioManager.
+        InputMediaDevice selectedInputDevice =
+                (InputMediaDevice) inputRouteManager.getSelectedInputDevice();
+        assertThat(selectedInputDevice.getAudioDeviceInfoType())
+                .isEqualTo(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+    }
+
+    @Test
+    public void getSelectedInputDevice_returnEmptyFromAudioManager() {
+        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+        when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+
+        final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceInfo[] devices = {info1, info2};
+        when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+        // Mock audioManager.getDevicesForAttributes returns empty list of audioDeviceAttributes.
+        List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+                .thenReturn(attributesOfSelectedInputDevices);
+
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // The selected input device has default type AudioDeviceInfo.TYPE_BUILTIN_MIC.
+        InputMediaDevice selectedInputDevice =
+                (InputMediaDevice) inputRouteManager.getSelectedInputDevice();
+        assertThat(selectedInputDevice.getAudioDeviceInfoType())
+                .isEqualTo(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+    }
+
+    @Test
     public void getMaxInputGain_returnMaxInputGain() {
         assertThat(mInputRouteManager.getMaxInputGain()).isEqualTo(15);
     }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 40a8199..d710939 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -86,6 +86,7 @@
         Settings.Secure.DOUBLE_TAP_TO_WAKE,
         Settings.Secure.WAKE_GESTURE_ENABLED,
         Settings.Secure.LONG_PRESS_TIMEOUT,
+        Settings.Secure.KEY_REPEAT_ENABLED,
         Settings.Secure.KEY_REPEAT_TIMEOUT_MS,
         Settings.Secure.KEY_REPEAT_DELAY_MS,
         Settings.Secure.CAMERA_GESTURE_DISABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 3b9c683..fa16a44 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -133,6 +133,7 @@
         VALIDATORS.put(Secure.DOUBLE_TAP_TO_WAKE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.WAKE_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.LONG_PRESS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR);
+        VALIDATORS.put(Secure.KEY_REPEAT_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.KEY_REPEAT_TIMEOUT_MS, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.KEY_REPEAT_DELAY_MS, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.CAMERA_GESTURE_DISABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 855ebe3..c49ffb4 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1063,6 +1063,13 @@
 }
 
 flag {
+  name: "communal_widget_resizing"
+  namespace: "systemui"
+  description: "Allow resizing of widgets on glanceable hub"
+  bug: "368053818"
+}
+
+flag {
   name: "app_clips_backlinks"
   namespace: "systemui"
   description: "Enables Backlinks improvement feature in App Clips"
@@ -1415,3 +1422,13 @@
    description: "Allow non-touchscreen devices to bypass falsing"
    bug: "319809270"
 }
+
+flag {
+   name: "media_projection_dialog_behind_lockscreen"
+   namespace: "systemui"
+   description: "Ensure MediaProjection Dialog appears behind the lockscreen"
+   bug: "351409536"
+   metadata {
+       purpose: PURPOSE_BUGFIX
+   }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 70529cc..ee65fbd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -21,6 +21,8 @@
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.internal.logging.uiEventLogger
+import com.android.internal.logging.uiEventLoggerFake
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
 import com.android.systemui.SysuiTestCase
@@ -28,6 +30,7 @@
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.EditModeState
 import com.android.systemui.coroutines.collectLastValue
@@ -35,7 +38,6 @@
 import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
 import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -93,6 +95,7 @@
                         bgScope = applicationCoroutineScope,
                         mainDispatcher = testDispatcher,
                         centralSurfacesOpt = centralSurfacesOptional,
+                        uiEventLogger = uiEventLoggerFake,
                     )
                     .apply { start() }
 
@@ -119,7 +122,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.GONE,
-                    testScope = this
+                    testScope = this,
                 )
 
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -140,7 +143,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.GONE,
-                    testScope = this
+                    testScope = this,
                 )
 
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -161,7 +164,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.GONE,
-                    testScope = this
+                    testScope = this,
                 )
 
                 assertThat(scene).isEqualTo(CommunalScenes.Blank)
@@ -181,7 +184,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.ALTERNATE_BOUNCER,
                     to = KeyguardState.GONE,
-                    testScope = this
+                    testScope = this,
                 )
                 // Scene change will be handled in EditWidgetsActivity not here
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -200,7 +203,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GONE,
                     to = KeyguardState.LOCKSCREEN,
-                    testScope = this
+                    testScope = this,
                 )
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
             }
@@ -220,7 +223,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.OCCLUDED,
-                    testScope = this
+                    testScope = this,
                 )
                 assertThat(scene).isEqualTo(CommunalScenes.Blank)
             }
@@ -240,7 +243,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.OCCLUDED,
-                    testScope = this
+                    testScope = this,
                 )
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
             }
@@ -258,7 +261,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.LOCKSCREEN,
-                    testScope = this
+                    testScope = this,
                 )
                 assertThat(scene).isEqualTo(CommunalScenes.Blank)
             }
@@ -276,7 +279,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.OFF,
-                    testScope = this
+                    testScope = this,
                 )
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
 
@@ -298,7 +301,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.OFF,
-                    testScope = this
+                    testScope = this,
                 )
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
                 advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2)
@@ -307,7 +310,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.OFF,
                     to = KeyguardState.GLANCEABLE_HUB,
-                    testScope = this
+                    testScope = this,
                 )
 
                 advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
@@ -327,7 +330,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.LOCKSCREEN,
-                    testScope = this
+                    testScope = this,
                 )
                 updateDocked(true)
 
@@ -349,7 +352,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.GLANCEABLE_HUB,
                     to = KeyguardState.LOCKSCREEN,
-                    testScope = this
+                    testScope = this,
                 )
                 updateDocked(true)
 
@@ -361,7 +364,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.DREAMING,
-                    testScope = this
+                    testScope = this,
                 )
                 advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
                 assertThat(scene).isEqualTo(CommunalScenes.Blank)
@@ -511,6 +514,9 @@
 
                 advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
                 assertThat(scene).isEqualTo(CommunalScenes.Blank)
+                assertThat(uiEventLoggerFake.logs.first().eventId)
+                    .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT.id)
+                assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
             }
         }
 
@@ -526,7 +532,7 @@
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.DOZING,
                     to = KeyguardState.GLANCEABLE_HUB,
-                    testScope = this
+                    testScope = this,
                 )
 
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 361e768..8f9e238 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -17,11 +17,13 @@
 
 package com.android.systemui.keyboard.data.repository
 
-import android.hardware.input.InputManager
+import android.hardware.input.FakeInputManager
+import android.hardware.input.InputManager.InputDeviceListener
 import android.hardware.input.InputManager.KeyboardBacklightListener
 import android.hardware.input.KeyboardBacklightState
+import android.hardware.input.fakeInputManager
 import android.testing.TestableLooper
-import android.view.InputDevice
+import android.view.InputDevice.SOURCE_KEYBOARD
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -30,10 +32,7 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
 import com.android.systemui.keyboard.data.model.Keyboard
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.testKosmos
 import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
@@ -50,9 +49,10 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
-import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -60,10 +60,9 @@
 @RunWith(AndroidJUnit4::class)
 class KeyboardRepositoryTest : SysuiTestCase() {
 
-    @Captor
-    private lateinit var deviceListenerCaptor: ArgumentCaptor<InputManager.InputDeviceListener>
+    @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
     @Captor private lateinit var backlightListenerCaptor: ArgumentCaptor<KeyboardBacklightListener>
-    @Mock private lateinit var inputManager: InputManager
+    private lateinit var fakeInputManager: FakeInputManager
 
     private lateinit var underTest: KeyboardRepository
     private lateinit var dispatcher: CoroutineDispatcher
@@ -73,16 +72,14 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
-        whenever(inputManager.getInputDevice(any())).then { invocation ->
-            val id = invocation.arguments.first()
-            INPUT_DEVICES_MAP[id]
-        }
+        fakeInputManager = testKosmos().fakeInputManager
         dispatcher = StandardTestDispatcher()
         testScope = TestScope(dispatcher)
         val handler = FakeHandler(TestableLooper.get(this).looper)
-        inputDeviceRepo = InputDeviceRepository(handler, testScope.backgroundScope, inputManager)
-        underTest = KeyboardRepositoryImpl(dispatcher, inputManager, inputDeviceRepo)
+        inputDeviceRepo =
+            InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager)
+        underTest =
+            KeyboardRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)
     }
 
     @Test
@@ -95,7 +92,7 @@
     @Test
     fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() =
         testScope.runTest {
-            whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
             val initialValue = underTest.isAnyKeyboardConnected.first()
             assertThat(initialValue).isTrue()
         }
@@ -103,74 +100,77 @@
     @Test
     fun emitsConnected_whenNewPhysicalKeyboardConnects() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
 
             assertThat(isKeyboardConnected).isTrue()
         }
 
     @Test
-    fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+    fun emitsDisconnected_whenDeviceNotFound() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
-
-            deviceListener.onInputDeviceAdded(NULL_DEVICE_ID)
+            fakeInputManager.addDevice(NULL_DEVICE_ID, SOURCE_KEYBOARD, isNotFound = true)
             assertThat(isKeyboardConnected).isFalse()
         }
 
     @Test
     fun emitsDisconnected_whenKeyboardDisconnects() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
             assertThat(isKeyboardConnected).isTrue()
 
-            deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.removeDevice(PHYSICAL_FULL_KEYBOARD_ID)
             assertThat(isKeyboardConnected).isFalse()
         }
 
-    private suspend fun captureDeviceListener(): InputManager.InputDeviceListener {
+    private suspend fun captureDeviceListener() {
         underTest.isAnyKeyboardConnected.first()
-        verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
-        return deviceListenerCaptor.value
+        verify(fakeInputManager.inputManager)
+            .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
+        fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
     }
 
     @Test
     fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(
+                PHYSICAL_NOT_FULL_KEYBOARD_ID,
+                isFullKeyboard = false
+            )
             assertThat(isKeyboardConnected).isFalse()
 
-            deviceListener.onInputDeviceAdded(VIRTUAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
             assertThat(isKeyboardConnected).isFalse()
         }
 
     @Test
     fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
 
-            deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.removeDevice(PHYSICAL_FULL_KEYBOARD_ID)
             assertThat(isKeyboardConnected).isFalse()
         }
 
     @Test
     fun emitsConnected_whenAnotherDeviceDisconnects() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
-            deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.removeDevice(VIRTUAL_FULL_KEYBOARD_ID)
 
             assertThat(isKeyboardConnected).isTrue()
         }
@@ -178,12 +178,12 @@
     @Test
     fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() =
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
-            deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
-            deviceListener.onInputDeviceRemoved(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.removeDevice(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
 
             assertThat(isKeyboardConnected).isTrue()
         }
@@ -195,7 +195,7 @@
             // subscribed to and listener is actually registered in inputManager
             val backlight by collectLastValueImmediately(underTest.backlight)
 
-            verify(inputManager)
+            verify(fakeInputManager.inputManager)
                 .registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
 
             backlightListenerCaptor.value.onBacklightChanged(current = 1, max = 5)
@@ -217,7 +217,7 @@
     fun keyboardBacklightValuesNotPassed_fromBacklightListener_whenNotTriggeredByKeyPress() {
         testScope.runTest {
             val backlight by collectLastValueImmediately(underTest.backlight)
-            verify(inputManager)
+            verify(fakeInputManager.inputManager)
                 .registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
 
             backlightListenerCaptor.value.onBacklightChanged(
@@ -233,7 +233,7 @@
     fun passesKeyboardBacklightValues_fromBacklightListener_whenTriggeredByKeyPress() {
         testScope.runTest {
             val backlight by collectLastValueImmediately(underTest.backlight)
-            verify(inputManager)
+            verify(fakeInputManager.inputManager)
                 .registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
 
             backlightListenerCaptor.value.onBacklightChanged(
@@ -248,14 +248,11 @@
     @Test
     fun passessAllKeyboards_thatWereAlreadyConnectedOnInitialization() {
         testScope.runTest {
-            whenever(inputManager.inputDeviceIds)
-                .thenReturn(
-                    intArrayOf(
-                        PHYSICAL_FULL_KEYBOARD_ID,
-                        ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
-                        VIRTUAL_FULL_KEYBOARD_ID // not a physical keyboard - that's why result is 2
-                    )
-                )
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            // not a physical keyboard - that's why result is 2
+            fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+
             val keyboards by collectValues(underTest.newlyConnectedKeyboard)
 
             assertThat(keyboards).hasSize(2)
@@ -265,9 +262,9 @@
     @Test
     fun passesNewlyConnectedKeyboard() {
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID, VENDOR_ID, PRODUCT_ID)
 
             assertThat(underTest.newlyConnectedKeyboard.first())
                 .isEqualTo(Keyboard(VENDOR_ID, PRODUCT_ID))
@@ -277,13 +274,12 @@
     @Test
     fun emitsOnlyNewlyConnectedKeyboards() {
         testScope.runTest {
-            whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
             underTest.newlyConnectedKeyboard.first()
-            verify(inputManager)
-                .registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
-            val deviceListener = deviceListenerCaptor.value
 
-            deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            captureDeviceListener()
+
+            fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
             val keyboards by collectValues(underTest.newlyConnectedKeyboard)
 
             assertThat(keyboards).hasSize(1)
@@ -293,14 +289,11 @@
     @Test
     fun stillEmitsNewKeyboardEvenIfFlowWasSubscribedAfterOtherFlows() {
         testScope.runTest {
-            whenever(inputManager.inputDeviceIds)
-                .thenReturn(
-                    intArrayOf(
-                        PHYSICAL_FULL_KEYBOARD_ID,
-                        ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
-                        VIRTUAL_FULL_KEYBOARD_ID // not a physical keyboard - that's why result is 2
-                    )
-                )
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+            // not a physical keyboard - that's why result is 2
+            fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+
             collectLastValueImmediately(underTest.isAnyKeyboardConnected)
 
             // let's pretend second flow is subscribed after some delay
@@ -314,12 +307,12 @@
     @Test
     fun emitsKeyboardWhenItWasReconnected() {
         testScope.runTest {
-            val deviceListener = captureDeviceListener()
+            captureDeviceListener()
             val keyboards by collectValues(underTest.newlyConnectedKeyboard)
 
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
-            deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
-            deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.removeDevice(PHYSICAL_FULL_KEYBOARD_ID)
+            fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
 
             assertThat(keyboards).hasSize(2)
         }
@@ -339,30 +332,13 @@
 
     private companion object {
         private const val PHYSICAL_FULL_KEYBOARD_ID = 1
-        private const val VIRTUAL_FULL_KEYBOARD_ID = 2
+        private const val VIRTUAL_FULL_KEYBOARD_ID = -2 // Virtual keyboards has id with minus value
         private const val PHYSICAL_NOT_FULL_KEYBOARD_ID = 3
         private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4
-        private const val NULL_DEVICE_ID = 5
+        private const val NULL_DEVICE_ID = -5
 
         private const val VENDOR_ID = 99
         private const val PRODUCT_ID = 101
-
-        private val INPUT_DEVICES_MAP: Map<Int, InputDevice> =
-            mapOf(
-                PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true),
-                VIRTUAL_FULL_KEYBOARD_ID to inputDevice(virtual = true, fullKeyboard = true),
-                PHYSICAL_NOT_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = false),
-                ANOTHER_PHYSICAL_FULL_KEYBOARD_ID to
-                    inputDevice(virtual = false, fullKeyboard = true)
-            )
-
-        private fun inputDevice(virtual: Boolean, fullKeyboard: Boolean): InputDevice =
-            mock<InputDevice>().also {
-                whenever(it.isVirtual).thenReturn(virtual)
-                whenever(it.isFullKeyboard).thenReturn(fullKeyboard)
-                whenever(it.vendorId).thenReturn(VENDOR_ID)
-                whenever(it.productId).thenReturn(PRODUCT_ID)
-            }
     }
 
     private class TestBacklightState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
index d594f3a..62d0625 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
@@ -121,7 +121,7 @@
                 MediaData(
                     userId = USER_ID,
                     instanceId = InstanceId.fakeInstanceId(2),
-                    artist = ARTIST
+                    artist = ARTIST,
                 )
 
             mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
@@ -145,10 +145,17 @@
 
         val clickIntent = mock<PendingIntent> { whenever(it.isActivity).thenReturn(true) }
         val expandable = mock<Expandable>()
+        val activityController = mock<ActivityTransitionAnimator.Controller>()
+        whenever(expandable.activityTransitionController(any())).thenReturn(activityController)
 
         underTest.startClickIntent(expandable, clickIntent, SMARTSPACE_CARD_CLICK_EVENT, 1)
 
-        verify(clickIntent).send(any<Bundle>())
+        verify(activityStarter)
+            .startPendingIntentMaybeDismissingKeyguard(
+                eq(clickIntent),
+                eq(null),
+                eq(activityController),
+            )
     }
 
     @Test
@@ -174,7 +181,7 @@
                 mediaData.appUid,
                 surface = SURFACE,
                 cardinality = 2,
-                rank = 1
+                rank = 1,
             )
         verify(activityStarter)
             .postStartActivityDismissingKeyguard(eq(clickIntent), eq(activityController))
@@ -232,7 +239,7 @@
                 eq(true),
                 eq(dialogTransitionController),
                 eq(null),
-                eq(null)
+                eq(null),
             )
     }
 
@@ -248,7 +255,7 @@
             .createBroadcastDialogWithController(
                 eq(APP_NAME),
                 eq(PACKAGE_NAME),
-                eq(dialogTransitionController)
+                eq(dialogTransitionController),
             )
     }
 
@@ -279,7 +286,7 @@
                 anyInt(),
                 anyInt(),
                 anyInt(),
-                anyBoolean()
+                anyBoolean(),
             )
         verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
     }
@@ -307,7 +314,7 @@
                 mediaData.appUid,
                 surface = SURFACE,
                 cardinality = 2,
-                rank = 1
+                rank = 1,
             )
         verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
index 9558e5d..0122028 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
@@ -24,26 +24,25 @@
 import android.media.session.PlaybackState
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
 import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
 import com.android.systemui.media.controls.util.mediaInstanceId
 import com.android.systemui.statusbar.notificationLockscreenUserManager
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -52,30 +51,31 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
+    private val mediaDataFilter = kosmos.mediaDataFilter
     private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
     private val packageManager = kosmos.packageManager
     private val drawable = context.getDrawable(R.drawable.ic_media_play)
-    private val instanceId: InstanceId = kosmos.mediaInstanceId
-
+    private val instanceId = kosmos.mediaInstanceId
     private val underTest: MediaControlViewModel = kosmos.mediaControlViewModel
 
+    @Before
+    fun setUp() {
+        whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
+        whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
+            .thenReturn(drawable)
+        whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
+            .thenReturn(ApplicationInfo())
+        whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
+        whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+        whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+        context.setMockPackageManager(packageManager)
+    }
+
     @Test
     fun addMediaControl_mediaControlViewModelIsLoaded() =
         testScope.runTest {
-            whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
-            whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
-                .thenReturn(drawable)
-            whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
-                .thenReturn(ApplicationInfo())
-            whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
-            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
-            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
             val playerModel by collectLastValue(underTest.player)
-
-            context.setMockPackageManager(packageManager)
-
-            val mediaData = initMediaData()
+            val mediaData = initMediaData(ARTIST, TITLE)
 
             mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
 
@@ -88,7 +88,51 @@
             assertThat(playerModel?.playTurbulenceNoise).isFalse()
         }
 
-    private fun initMediaData(): MediaData {
+    @Test
+    fun emitDuplicateMediaControls_mediaControlIsNotBound() =
+        testScope.runTest {
+            val playerModel by collectLastValue(underTest.player)
+            val mediaData = initMediaData(ARTIST, TITLE)
+
+            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+            assertThat(playerModel).isNotNull()
+            assertThat(playerModel?.titleName).isEqualTo(TITLE)
+            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
+            assertThat(underTest.isNewPlayer(playerModel!!)).isTrue()
+
+            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+            assertThat(playerModel).isNotNull()
+            assertThat(playerModel?.titleName).isEqualTo(TITLE)
+            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
+            assertThat(underTest.isNewPlayer(playerModel!!)).isFalse()
+        }
+
+    @Test
+    fun emitDifferentMediaControls_mediaControlIsBound() =
+        testScope.runTest {
+            val playerModel by collectLastValue(underTest.player)
+            var mediaData = initMediaData(ARTIST, TITLE)
+
+            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+            assertThat(playerModel).isNotNull()
+            assertThat(playerModel?.titleName).isEqualTo(TITLE)
+            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
+            assertThat(underTest.isNewPlayer(playerModel!!)).isTrue()
+
+            mediaData = initMediaData(ARTIST_2, TITLE_2)
+
+            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+            assertThat(playerModel).isNotNull()
+            assertThat(playerModel?.titleName).isEqualTo(TITLE_2)
+            assertThat(playerModel?.artistName).isEqualTo(ARTIST_2)
+            assertThat(underTest.isNewPlayer(playerModel!!)).isTrue()
+        }
+
+    private fun initMediaData(artist: String, title: String): MediaData {
         val device = MediaDeviceData(true, null, DEVICE_NAME, null, showBroadcastButton = true)
 
         // Create media session
@@ -111,12 +155,12 @@
 
         return MediaData(
             userId = USER_ID,
-            artist = ARTIST,
-            song = TITLE,
+            artist = artist,
+            song = title,
             packageName = PACKAGE,
             token = session.sessionToken,
             device = device,
-            instanceId = instanceId
+            instanceId = instanceId,
         )
     }
 
@@ -127,6 +171,8 @@
         private const val PACKAGE = "PKG"
         private const val ARTIST = "ARTIST"
         private const val TITLE = "TITLE"
+        private const val ARTIST_2 = "ARTIST_2"
+        private const val TITLE_2 = "TITLE_2"
         private const val DEVICE_NAME = "DEVICE_NAME"
         private const val SESSION_KEY = "SESSION_KEY"
         private const val SESSION_ARTIST = "SESSION_ARTIST"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/NotificationHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/NotificationHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/NotificationHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/NotificationHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleProviderTestable.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleProviderTestable.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/PeopleProviderTestable.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleProviderTestable.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/SharedPreferencesHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/SharedPreferencesHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSContainerImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSContainerImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TileStateToProtoTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TileStateToProtoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TouchAnimatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TouchAnimatorTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/TouchAnimatorTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TouchAnimatorTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/UserSettingObserverTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/UserSettingObserverTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CustomTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CustomTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileColorPickerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileColorPickerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/external/TileColorPickerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileColorPickerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NfcTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NfcTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index ee6223f..08a7c39 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -19,11 +19,13 @@
 import android.provider.Settings
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.communalSceneKtfRefactor
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.CommunalTransitionKeys
 import com.android.systemui.communal.shared.model.EditModeState
@@ -84,6 +86,7 @@
     @Application private val applicationScope: CoroutineScope,
     @Background private val bgScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
+    private val uiEventLogger: UiEventLogger,
 ) : CoreStartable {
     private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT
 
@@ -184,6 +187,7 @@
                             CommunalScenes.Blank,
                             "dream started after timeout",
                         )
+                        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT)
                     }
                 }
         }
@@ -212,6 +216,7 @@
                             newScene = CommunalScenes.Blank,
                             loggingReason = "hub timeout",
                         )
+                        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT)
                     }
                     timeoutJob = null
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8f913ff..78a8a42 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -113,6 +113,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.textclassifier.TextClassificationManager;
 
+import androidx.annotation.NonNull;
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
 import androidx.core.app.NotificationManagerCompat;
 
@@ -718,6 +719,19 @@
 
     @Provides
     @Singleton
+    static ViewCaptureAwareWindowManager.Factory viewCaptureAwareWindowManagerFactory(
+            Lazy<ViewCapture> daggerLazyViewCapture) {
+        return new ViewCaptureAwareWindowManager.Factory() {
+            @NonNull
+            @Override
+            public ViewCaptureAwareWindowManager create(@NonNull WindowManager windowManager) {
+                return provideViewCaptureAwareWindowManager(windowManager, daggerLazyViewCapture);
+            }
+        };
+    }
+
+    @Provides
+    @Singleton
     static PermissionManager providePermissionManager(Context context) {
         PermissionManager pm = context.getSystemService(PermissionManager.class);
         if (pm != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 1a0525d..ba23eb3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -346,6 +346,7 @@
     private boolean mShuttingDown;
     private boolean mDozing;
     private boolean mAnimatingScreenOff;
+    private boolean mIgnoreDismiss;
     private final Context mContext;
     private final FalsingCollector mFalsingCollector;
 
@@ -627,18 +628,20 @@
         public void onUserSwitching(int userId) {
             Log.d(TAG, String.format("onUserSwitching %d", userId));
             synchronized (KeyguardViewMediator.this) {
+                mIgnoreDismiss = true;
                 notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
                 resetKeyguardDonePendingLocked();
-                dismiss(null /* callback */, null /* message */);
+                resetStateLocked();
                 adjustStatusBarLocked();
             }
         }
 
         @Override
         public void onUserSwitchComplete(int userId) {
+            mIgnoreDismiss = false;
             Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
-            // We are calling dismiss again and with a delay as there are race conditions
-            // in some scenarios caused by async layout listeners
+            // We are calling dismiss with a delay as there are race conditions in some scenarios
+            // caused by async layout listeners
             mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
         }
 
@@ -2442,6 +2445,10 @@
     }
 
     public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+        if (mIgnoreDismiss) {
+            android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
+            return;
+        }
         mHandler.obtainMessage(DISMISS, new DismissMessage(callback, message)).sendToTarget();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index deb0b2d..b5f6b41 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -261,7 +261,10 @@
                             ->
                             if (biometricMessage?.message != null) {
                                 chipbarCoordinator!!.displayView(
-                                    createChipbarInfo(biometricMessage.message, R.drawable.ic_lock)
+                                    createChipbarInfo(
+                                        biometricMessage.message,
+                                        R.drawable.ic_lock,
+                                    )
                                 )
                             } else {
                                 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
@@ -324,16 +327,12 @@
                                     .getDimensionPixelSize(R.dimen.shelf_appear_translation)
                                     .stateIn(this)
                             viewModel.isNotifIconContainerVisible.collect { isVisible ->
-                                if (isVisible.value) {
-                                    blueprintViewModel.refreshBlueprint()
-                                } else {
-                                    childViews[aodNotificationIconContainerId]
-                                        ?.setAodNotifIconContainerIsVisible(
-                                            isVisible,
-                                            iconsAppearTranslationPx.value,
-                                            screenOffAnimationController,
-                                        )
-                                }
+                                childViews[aodNotificationIconContainerId]
+                                    ?.setAodNotifIconContainerIsVisible(
+                                        isVisible,
+                                        iconsAppearTranslationPx.value,
+                                        screenOffAnimationController,
+                                    )
                             }
                         }
 
@@ -383,7 +382,7 @@
                                 if (msdlFeedback()) {
                                     msdlPlayer?.playToken(
                                         MSDLToken.UNLOCK,
-                                        authInteractionProperties,
+                                        authInteractionProperties
                                     )
                                 } else {
                                     vibratorHelper.performHapticFeedback(
@@ -399,7 +398,7 @@
                                 if (msdlFeedback()) {
                                     msdlPlayer?.playToken(
                                         MSDLToken.FAILURE,
-                                        authInteractionProperties,
+                                        authInteractionProperties
                                     )
                                 } else {
                                     vibratorHelper.performHapticFeedback(
@@ -426,7 +425,7 @@
                     blueprintViewModel,
                     clockViewModel,
                     childViews,
-                    burnInParams,
+                    burnInParams
                 )
             )
 
@@ -465,7 +464,11 @@
      */
     private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
         return ChipbarInfo(
-            startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT),
+            startIcon =
+                TintedIcon(
+                    Icon.Resource(icon, null),
+                    ChipbarInfo.DEFAULT_ICON_TINT,
+                ),
             text = Text.Loaded(message),
             endItem = null,
             vibrationEffect = null,
@@ -496,7 +499,7 @@
             oldLeft: Int,
             oldTop: Int,
             oldRight: Int,
-            oldBottom: Int,
+            oldBottom: Int
         ) {
             // After layout, ensure the notifications are positioned correctly
             childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
@@ -512,7 +515,7 @@
                 viewModel.onNotificationContainerBoundsChanged(
                     notificationListPlaceholder.top.toFloat(),
                     notificationListPlaceholder.bottom.toFloat(),
-                    animate = shouldAnimate,
+                    animate = shouldAnimate
                 )
             }
 
@@ -528,7 +531,7 @@
                                         Int.MAX_VALUE
                                     } else {
                                         view.getTop()
-                                    },
+                                    }
                                 )
                             }
                         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index 4cf3c4e..c6efcfa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -25,18 +25,20 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-data class TransitionData(val config: Config, val start: Long = System.currentTimeMillis())
+data class TransitionData(
+    val config: Config,
+    val start: Long = System.currentTimeMillis(),
+)
 
 class KeyguardBlueprintViewModel
 @Inject
 constructor(
     @Main private val handler: Handler,
-    private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -74,9 +76,6 @@
             }
         }
 
-    fun refreshBlueprint(type: Type = Type.NoTransition) =
-        keyguardBlueprintInteractor.refreshBlueprint(type)
-
     fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) {
         runningTransitions.mutate()
 
@@ -96,7 +95,7 @@
                 Log.w(
                     TAG,
                     "runTransition: skipping ${transition::class.simpleName}: " +
-                        "currentPriority=$currentPriority; config=$config",
+                        "currentPriority=$currentPriority; config=$config"
                 )
             }
             apply()
@@ -107,7 +106,7 @@
             Log.i(
                 TAG,
                 "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config",
+                    "currentPriority=$currentPriority; config=$config"
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 130868d..1f339dd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
 import com.android.systemui.media.controls.domain.pipeline.getNotificationActions
+import com.android.systemui.media.controls.shared.MediaLogger
 import com.android.systemui.media.controls.shared.model.MediaControlModel
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.util.MediaSmartspaceLogger
@@ -59,6 +60,7 @@
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     private val mediaOutputDialogManager: MediaOutputDialogManager,
     private val broadcastDialogController: BroadcastDialogController,
+    private val mediaLogger: MediaLogger,
 ) {
 
     val mediaControl: Flow<MediaControlModel?> =
@@ -73,7 +75,7 @@
         instanceId: InstanceId,
         delayMs: Long,
         eventId: Int,
-        location: Int
+        location: Int,
     ): Boolean {
         logSmartspaceUserEvent(eventId, location)
         val dismissed =
@@ -81,7 +83,7 @@
         if (!dismissed) {
             Log.w(
                 TAG,
-                "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}"
+                "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}",
             )
         }
         return dismissed
@@ -120,20 +122,20 @@
         expandable: Expandable,
         clickIntent: PendingIntent,
         eventId: Int,
-        location: Int
+        location: Int,
     ) {
         logSmartspaceUserEvent(eventId, location)
-        if (!launchOverLockscreen(clickIntent)) {
+        if (!launchOverLockscreen(expandable, clickIntent)) {
             activityStarter.postStartActivityDismissingKeyguard(
                 clickIntent,
-                expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER)
+                expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER),
             )
         }
     }
 
     fun startDeviceIntent(deviceIntent: PendingIntent) {
         if (deviceIntent.isActivity) {
-            if (!launchOverLockscreen(deviceIntent)) {
+            if (!launchOverLockscreen(expandable = null, deviceIntent)) {
                 activityStarter.postStartActivityDismissingKeyguard(deviceIntent)
             }
         } else {
@@ -141,20 +143,33 @@
         }
     }
 
-    private fun launchOverLockscreen(pendingIntent: PendingIntent): Boolean {
+    private fun launchOverLockscreen(
+        expandable: Expandable?,
+        pendingIntent: PendingIntent,
+    ): Boolean {
         val showOverLockscreen =
             keyguardStateController.isShowing &&
                 activityIntentHelper.wouldPendingShowOverLockscreen(
                     pendingIntent,
-                    lockscreenUserManager.currentUserId
+                    lockscreenUserManager.currentUserId,
                 )
         if (showOverLockscreen) {
             try {
-                val options = BroadcastOptions.makeBasic()
-                options.isInteractive = true
-                options.pendingIntentBackgroundActivityStartMode =
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-                pendingIntent.send(options.toBundle())
+                if (expandable != null) {
+                    activityStarter.startPendingIntentMaybeDismissingKeyguard(
+                        pendingIntent,
+                        /* intentSentUiThreadCallback = */ null,
+                        expandable.activityTransitionController(
+                            Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER
+                        ),
+                    )
+                } else {
+                    val options = BroadcastOptions.makeBasic()
+                    options.isInteractive = true
+                    options.pendingIntentBackgroundActivityStartMode =
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                    pendingIntent.send(options.toBundle())
+                }
             } catch (e: PendingIntent.CanceledException) {
                 Log.e(TAG, "pending intent of $instanceId was canceled")
             }
@@ -166,7 +181,7 @@
     fun startMediaOutputDialog(
         expandable: Expandable,
         packageName: String,
-        token: MediaSession.Token? = null
+        token: MediaSession.Token? = null,
     ) {
         mediaOutputDialogManager.createAndShowWithController(
             packageName,
@@ -180,7 +195,7 @@
         broadcastDialogController.createBroadcastDialogWithController(
             broadcastApp,
             packageName,
-            expandable.dialogTransitionController()
+            expandable.dialogTransitionController(),
         )
     }
 
@@ -188,10 +203,14 @@
         repository.logSmartspaceCardUserEvent(
             eventId,
             MediaSmartspaceLogger.getSurface(location),
-            instanceId = instanceId
+            instanceId = instanceId,
         )
     }
 
+    fun logMediaControlIsBound(artistName: CharSequence, songName: CharSequence) {
+        mediaLogger.logMediaControlIsBound(instanceId, artistName, songName)
+    }
+
     private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? {
         return dialogTransitionController(
             cuj =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
index 7d20e17..88c47ba 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
@@ -36,7 +36,7 @@
                 bool1 = active
                 str2 = reason
             },
-            { "add media $str1, active: $bool1, reason: $str2" }
+            { "add media $str1, active: $bool1, reason: $str2" },
         )
     }
 
@@ -48,7 +48,7 @@
                 str1 = instanceId.toString()
                 str2 = reason
             },
-            { "removing media $str1, reason: $str2" }
+            { "removing media $str1, reason: $str2" },
         )
     }
 
@@ -61,7 +61,7 @@
                 bool1 = isActive
                 str2 = reason
             },
-            { "add recommendation $str1, active $bool1, reason: $str2" }
+            { "add recommendation $str1, active $bool1, reason: $str2" },
         )
     }
 
@@ -74,7 +74,7 @@
                 bool1 = immediately
                 str2 = reason
             },
-            { "removing recommendation $str1, immediate=$bool1, reason: $str2" }
+            { "removing recommendation $str1, immediate=$bool1, reason: $str2" },
         )
     }
 
@@ -83,7 +83,7 @@
             TAG,
             LogLevel.DEBUG,
             { str1 = instanceId.toString() },
-            { "adding media card $str1 to carousel" }
+            { "adding media card $str1 to carousel" },
         )
     }
 
@@ -92,7 +92,7 @@
             TAG,
             LogLevel.DEBUG,
             { str1 = instanceId.toString() },
-            { "removing media card $str1 from carousel" }
+            { "removing media card $str1 from carousel" },
         )
     }
 
@@ -101,7 +101,7 @@
             TAG,
             LogLevel.DEBUG,
             { str1 = key },
-            { "adding recommendation card $str1 to carousel" }
+            { "adding recommendation card $str1 to carousel" },
         )
     }
 
@@ -110,7 +110,7 @@
             TAG,
             LogLevel.DEBUG,
             { str1 = key },
-            { "removing recommendation card $str1 from carousel" }
+            { "removing recommendation card $str1 from carousel" },
         )
     }
 
@@ -119,7 +119,24 @@
             TAG,
             LogLevel.DEBUG,
             { str1 = key },
-            { "duplicate media notification $str1 posted" }
+            { "duplicate media notification $str1 posted" },
+        )
+    }
+
+    fun logMediaControlIsBound(
+        instanceId: InstanceId,
+        artistName: CharSequence,
+        title: CharSequence,
+    ) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = instanceId.toString()
+                str2 = artistName.toString()
+                str3 = title.toString()
+            },
+            { "binding media control, instance id= $str1, artist= $str2, title= $str3" },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
index 6373fed..4055818 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
@@ -80,16 +80,19 @@
         mediaCard.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
-                    viewModel.player.collectLatest { playerViewModel ->
-                        playerViewModel?.let {
-                            bindMediaCard(
-                                viewHolder,
-                                viewController,
-                                it,
-                                falsingManager,
-                                backgroundDispatcher,
-                                mainDispatcher,
-                            )
+                    viewModel.player.collectLatest { player ->
+                        player?.let {
+                            if (viewModel.isNewPlayer(it)) {
+                                bindMediaCard(
+                                    viewHolder,
+                                    viewController,
+                                    it,
+                                    falsingManager,
+                                    backgroundDispatcher,
+                                    mainDispatcher,
+                                )
+                                viewModel.onMediaControlIsBound(it.artistName, it.titleName)
+                            }
                         }
                     }
                 }
@@ -143,7 +146,7 @@
             viewHolder,
             viewModel.outputSwitcher,
             viewController,
-            falsingManager
+            falsingManager,
         )
         bindGutsViewModel(viewHolder, viewModel, viewController, falsingManager)
         bindActionButtons(viewHolder, viewModel, viewController, falsingManager)
@@ -157,7 +160,7 @@
             viewController,
             backgroundDispatcher,
             mainDispatcher,
-            isSongUpdated
+            isSongUpdated,
         )
 
         if (viewModel.playTurbulenceNoise) {
@@ -259,12 +262,12 @@
                 if (buttonView.id == R.id.actionPrev) {
                     viewController.setUpPrevButtonInfo(
                         buttonModel.isEnabled,
-                        buttonModel.notVisibleValue
+                        buttonModel.notVisibleValue,
                     )
                 } else if (buttonView.id == R.id.actionNext) {
                     viewController.setUpNextButtonInfo(
                         buttonModel.isEnabled,
-                        buttonModel.notVisibleValue
+                        buttonModel.notVisibleValue,
                     )
                 }
                 val animHandler = (buttonView.tag ?: AnimationBindHandler()) as AnimationBindHandler
@@ -295,7 +298,7 @@
                         viewController.collapsedLayout,
                         visible,
                         buttonModel.notVisibleValue,
-                        buttonModel.showInCollapsed
+                        buttonModel.showInCollapsed,
                     )
                 }
             }
@@ -350,7 +353,7 @@
                         createTouchRippleAnimation(
                             button,
                             viewController.colorSchemeTransition,
-                            multiRippleView
+                            multiRippleView,
                         )
                     )
 
@@ -382,12 +385,12 @@
                 setVisibleAndAlpha(
                     expandedSet,
                     R.id.media_explicit_indicator,
-                    viewModel.isExplicitVisible
+                    viewModel.isExplicitVisible,
                 )
                 setVisibleAndAlpha(
                     collapsedSet,
                     R.id.media_explicit_indicator,
-                    viewModel.isExplicitVisible
+                    viewModel.isExplicitVisible,
                 )
 
                 // refreshState is required here to resize the text views (and prevent ellipsis)
@@ -398,7 +401,7 @@
                 // something is incorrectly bound, but needs to be run if other elements were
                 // updated while the enter animation was running
                 viewController.refreshState()
-            }
+            },
         )
     }
 
@@ -427,7 +430,7 @@
                         viewModel.backgroundCover!!,
                         viewModel.colorScheme,
                         width,
-                        height
+                        height,
                     )
                 } else {
                     ColorDrawable(Color.TRANSPARENT)
@@ -493,7 +496,7 @@
         transitionDrawable: TransitionDrawable,
         layer: Int,
         targetWidth: Int,
-        targetHeight: Int
+        targetHeight: Int,
     ) {
         val drawable = transitionDrawable.getDrawable(layer) ?: return
         val width = drawable.intrinsicWidth
@@ -509,7 +512,7 @@
         artworkIcon: android.graphics.drawable.Icon,
         mutableColorScheme: ColorScheme,
         width: Int,
-        height: Int
+        height: Int,
     ): LayerDrawable {
         val albumArt = MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height)
         return MediaArtworkHelper.setUpGradientColorOnDrawable(
@@ -517,7 +520,7 @@
             context.getDrawable(R.drawable.qs_media_scrim)?.mutate() as GradientDrawable,
             mutableColorScheme,
             MEDIA_PLAYER_SCRIM_START_ALPHA,
-            MEDIA_PLAYER_SCRIM_END_ALPHA
+            MEDIA_PLAYER_SCRIM_END_ALPHA,
         )
     }
 
@@ -544,7 +547,7 @@
     private fun createTouchRippleAnimation(
         button: ImageButton,
         colorSchemeTransition: ColorSchemeTransition,
-        multiRippleView: MultiRippleView
+        multiRippleView: MultiRippleView,
     ): RippleAnimation {
         val maxSize = (multiRippleView.width * 2).toFloat()
         return RippleAnimation(
@@ -562,7 +565,7 @@
                 baseRingFadeParams = null,
                 sparkleRingFadeParams = null,
                 centerFillFadeParams = null,
-                shouldDistort = false
+                shouldDistort = false,
             )
         )
     }
@@ -596,7 +599,7 @@
         set: ConstraintSet,
         resId: Int,
         visible: Boolean,
-        notVisibleValue: Int
+        notVisibleValue: Int,
     ) {
         set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else notVisibleValue)
         set.setAlpha(resId, if (visible) 1.0f else 0.0f)
@@ -618,7 +621,7 @@
         collapsedSet: ConstraintSet,
         visible: Boolean,
         notVisibleValue: Int,
-        showInCollapsed: Boolean
+        showInCollapsed: Boolean,
     ) {
         if (notVisibleValue == ConstraintSet.INVISIBLE) {
             // Since time views should appear instead of buttons.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
index 82099e6..3f22d54 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
@@ -32,4 +32,16 @@
     val buttonId: Int? = null,
     val isEnabled: Boolean,
     val onClicked: (Int) -> Unit,
-)
+) {
+    fun contentEquals(other: MediaActionViewModel?): Boolean {
+        return other?.let {
+            contentDescription == other.contentDescription &&
+                isVisibleWhenScrubbing == other.isVisibleWhenScrubbing &&
+                notVisibleValue == other.notVisibleValue &&
+                showInCollapsed == other.showInCollapsed &&
+                rebindId == other.rebindId &&
+                buttonId == other.buttonId &&
+                isEnabled == other.isEnabled
+        } ?: false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
index 64820e0..104d155 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
@@ -69,18 +69,30 @@
                     mediaControl?.let { toViewModel(it) }
                 }
             }
-            .distinctUntilChanged()
+            .distinctUntilChanged { old, new ->
+                (new == null && old == null) || new?.contentEquals(old) ?: false
+            }
             .flowOn(backgroundDispatcher)
 
     private var isPlaying = false
     private var isAnyButtonClicked = false
     private var location = -1
+    private var playerViewModel: MediaPlayerViewModel? = null
+
+    fun isNewPlayer(viewModel: MediaPlayerViewModel): Boolean {
+        val contentEquals = playerViewModel?.contentEquals(viewModel) ?: false
+        return (!contentEquals).also { playerViewModel = viewModel }
+    }
+
+    fun onMediaControlIsBound(artistName: CharSequence, titleName: CharSequence) {
+        interactor.logMediaControlIsBound(artistName, titleName)
+    }
 
     private fun onDismissMediaData(
         token: Token?,
         uid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         logger.logLongPressDismiss(uid, packageName, instanceId)
         interactor.removeMediaControl(
@@ -88,7 +100,7 @@
             instanceId,
             MEDIA_PLAYER_ANIMATION_DELAY,
             SMARTSPACE_CARD_DISMISS_EVENT,
-            location
+            location,
         )
     }
 
@@ -99,7 +111,7 @@
                 applicationContext,
                 backgroundDispatcher,
                 model.artwork,
-                TAG
+                TAG,
             )
         val scheme =
             wallpaperColors?.let { ColorScheme(it, true, Style.CONTENT) }
@@ -107,7 +119,7 @@
                     applicationContext,
                     model.packageName,
                     TAG,
-                    Style.CONTENT
+                    Style.CONTENT,
                 )
                 ?: return null
 
@@ -131,7 +143,7 @@
                         R.string.controls_media_playing_item_description,
                         model.songName,
                         model.artistName,
-                        model.appName
+                        model.appName,
                     )
                 }
             },
@@ -157,7 +169,7 @@
                         expandable,
                         clickIntent,
                         SMARTSPACE_CARD_CLICK_EVENT,
-                        location
+                        location,
                     )
                 }
             },
@@ -177,7 +189,7 @@
                     }
                 }
             },
-            onLocationChanged = { location = it }
+            onLocationChanged = { location = it },
         )
     }
 
@@ -191,7 +203,7 @@
             device?.name?.let {
                 TextUtils.equals(
                     it,
-                    applicationContext.getString(R.string.broadcasting_description_is_broadcasting)
+                    applicationContext.getString(R.string.broadcasting_description_is_broadcasting),
                 )
             } ?: false
         val useDisabledAlpha =
@@ -236,19 +248,19 @@
                         logger.logOpenBroadcastDialog(
                             model.uid,
                             model.packageName,
-                            model.instanceId
+                            model.instanceId,
                         )
                         interactor.startBroadcastDialog(
                             expandable,
                             device?.name.toString(),
-                            model.packageName
+                            model.packageName,
                         )
                     } else {
                         logger.logOpenOutputSwitcher(model.uid, model.packageName, model.instanceId)
                         interactor.startMediaOutputDialog(
                             expandable,
                             model.packageName,
-                            model.token
+                            model.token,
                         )
                     }
                 } else {
@@ -257,10 +269,10 @@
                         ?: interactor.startMediaOutputDialog(
                             expandable,
                             model.packageName,
-                            model.token
+                            model.token,
                         )
                 }
-            }
+            },
         )
     }
 
@@ -270,7 +282,7 @@
                 if (model.isDismissible) {
                     applicationContext.getString(
                         R.string.controls_media_close_session,
-                        model.appName
+                        model.appName,
                     )
                 } else {
                     applicationContext.getString(R.string.controls_media_active_session)
@@ -304,7 +316,7 @@
                         model,
                         mediaButton.getActionById(buttonId),
                         buttonId,
-                        isScrubbingTimeEnabled
+                        isScrubbingTimeEnabled,
                     )
                 }
             }
@@ -319,7 +331,7 @@
         model: MediaControlModel,
         mediaAction: MediaAction?,
         buttonId: Int,
-        canShowScrubbingTimeViews: Boolean
+        canShowScrubbingTimeViews: Boolean,
     ): MediaActionViewModel {
         val showInCollapsed = SEMANTIC_ACTIONS_COMPACT.contains(buttonId)
         val hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId)
@@ -353,7 +365,7 @@
     private fun toNotifActionViewModel(
         model: MediaControlModel,
         mediaAction: MediaAction,
-        index: Int
+        index: Int,
     ): MediaActionViewModel {
         return MediaActionViewModel(
             icon = mediaAction.icon,
@@ -375,7 +387,7 @@
         uid: Int,
         packageName: String,
         instanceId: InstanceId,
-        action: Runnable
+        action: Runnable,
     ) {
         logger.logTapAction(id, uid, packageName, instanceId)
         interactor.logSmartspaceUserEvent(SMARTSPACE_CARD_CLICK_EVENT, location)
@@ -424,7 +436,7 @@
                 R.id.actionPrev,
                 R.id.actionNext,
                 R.id.action0,
-                R.id.action1
+                R.id.action1,
             )
 
         const val TURBULENCE_NOISE_PLAY_MS_DURATION = 7500L
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
index 9df9bcc..2a47a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
@@ -29,4 +29,15 @@
     val alpha: Float,
     val isVisible: Boolean,
     val onClicked: (Expandable) -> Unit,
-)
+) {
+    fun contentEquals(other: MediaOutputSwitcherViewModel?): Boolean {
+        return (other?.let {
+            isTapEnabled == other.isTapEnabled &&
+                deviceString == other.deviceString &&
+                isCurrentBroadcastApp == other.isCurrentBroadcastApp &&
+                isIntentValid == other.isIntentValid &&
+                alpha == other.alpha &&
+                isVisible == other.isVisible
+        } ?: false)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
index 96e7fc7..f4b0d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
@@ -43,4 +43,30 @@
     val onSeek: () -> Unit,
     val onBindSeekbar: (SeekBarViewModel) -> Unit,
     val onLocationChanged: (Int) -> Unit,
-)
+) {
+    fun contentEquals(other: MediaPlayerViewModel?): Boolean {
+        return other?.let {
+            other.backgroundCover == backgroundCover &&
+                appIcon == other.appIcon &&
+                useGrayColorFilter == other.useGrayColorFilter &&
+                artistName == other.artistName &&
+                titleName == other.titleName &&
+                isExplicitVisible == other.isExplicitVisible &&
+                shouldAddGradient == other.shouldAddGradient &&
+                canShowTime == other.canShowTime &&
+                playTurbulenceNoise == other.playTurbulenceNoise &&
+                useSemanticActions == other.useSemanticActions &&
+                areActionsEqual(other.actionButtons) &&
+                outputSwitcher.contentEquals(other.outputSwitcher)
+        } ?: false
+    }
+
+    private fun areActionsEqual(other: List<MediaActionViewModel>): Boolean {
+        actionButtons.forEachIndexed { index, mediaActionViewModel ->
+            if (!mediaActionViewModel.contentEquals(other[index])) {
+                return false
+            }
+        }
+        return true
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 2cbc7575..f7b7353 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -157,7 +157,7 @@
     @VisibleForTesting
     boolean mNeedRefresh = false;
     private MediaController mMediaController;
-    private InputRouteManager mInputRouteManager;
+    @VisibleForTesting InputRouteManager mInputRouteManager;
     @VisibleForTesting
     Callback mCallback;
     @VisibleForTesting
@@ -927,7 +927,18 @@
     }
 
     public List<MediaDevice> getSelectedMediaDevice() {
-        return mLocalMediaManager.getSelectedMediaDevice();
+        if (!enableInputRouting()) {
+            return mLocalMediaManager.getSelectedMediaDevice();
+        }
+
+        // Add selected input device if input routing is supported.
+        List<MediaDevice> selectedDevices =
+                new ArrayList<>(mLocalMediaManager.getSelectedMediaDevice());
+        MediaDevice selectedInputDevice = mInputRouteManager.getSelectedInputDevice();
+        if (selectedInputDevice != null) {
+            selectedDevices.add(selectedInputDevice);
+        }
+        return selectedDevices;
     }
 
     List<MediaDevice> getDeselectableMediaDevice() {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 4251b81..8351597 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -34,6 +34,7 @@
 import android.app.Activity;
 import android.app.ActivityOptions.LaunchCookie;
 import android.app.AlertDialog;
+import android.app.KeyguardManager;
 import android.app.StatusBarManager;
 import android.app.compat.CompatChanges;
 import android.content.Context;
@@ -83,6 +84,7 @@
     private final StatusBarManager mStatusBarManager;
     private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
     private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
+    private final KeyguardManager mKeyguardManager;
 
     private String mPackageName;
     private int mUid;
@@ -101,11 +103,13 @@
             FeatureFlags featureFlags,
             Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver,
             StatusBarManager statusBarManager,
+            KeyguardManager keyguardManager,
             MediaProjectionMetricsLogger mediaProjectionMetricsLogger,
             ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate) {
         mFeatureFlags = featureFlags;
         mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
         mStatusBarManager = statusBarManager;
+        mKeyguardManager = keyguardManager;
         mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger;
         mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate;
     }
@@ -208,7 +212,14 @@
         }
 
         setUpDialog(mDialog);
-        mDialog.show();
+
+        boolean shouldDismissKeyguard =
+                com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen();
+        if (shouldDismissKeyguard && mKeyguardManager.isDeviceLocked()) {
+            requestDeviceUnlock();
+        } else {
+            mDialog.show();
+        }
 
         if (savedInstanceState == null) {
             mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(mUid);
@@ -332,6 +343,16 @@
         return false;
     }
 
+    private void requestDeviceUnlock() {
+        mKeyguardManager.requestDismissKeyguard(this,
+                new KeyguardManager.KeyguardDismissCallback() {
+                    @Override
+                    public void onDismissSucceeded() {
+                        mDialog.show();
+                    }
+                });
+    }
+
     private void grantMediaProjectionPermission(
             int screenShareMode, boolean hasCastingCapabilities) {
         try {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 526c64c..55943a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.dagger
 
+import android.content.Context
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
@@ -63,12 +65,18 @@
 
     @Binds abstract fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer
 
-    @Binds
-    abstract fun statusBarWindowController(
-        impl: StatusBarWindowControllerImpl
-    ): StatusBarWindowController
-
     companion object {
+
+        @Provides
+        @SysUISingleton
+        fun statusBarWindowController(
+            context: Context?,
+            viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager?,
+            factory: StatusBarWindowControllerImpl.Factory,
+        ): StatusBarWindowController {
+            return factory.create(context, viewCaptureAwareWindowManager)
+        }
+
         @Provides
         @SysUISingleton
         @OngoingCallLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 1a0327c..1ee7cf3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -28,7 +28,6 @@
 import static com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
@@ -52,23 +51,23 @@
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.animation.DelegateTransitionAnimatorController;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
+import com.android.systemui.statusbar.window.StatusBarWindowModule.InternalWindowViewInflater;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
 
-import java.util.Optional;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
 
-import javax.inject.Inject;
+import java.util.Optional;
 
 /**
  * Encapsulates all logic for the status bar window state management.
  */
-@SysUISingleton
 public class StatusBarWindowControllerImpl implements StatusBarWindowController {
     private static final String TAG = "StatusBarWindowController";
     private static final boolean DEBUG = false;
@@ -90,21 +89,20 @@
     private final WindowManager.LayoutParams mLpChanged;
     private final Binder mInsetsSourceOwner = new Binder();
 
-    @Inject
+    @AssistedInject
     public StatusBarWindowControllerImpl(
-            Context context,
-            @StatusBarWindowModule.InternalWindowView StatusBarWindowView statusBarWindowView,
-            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
+            @Assisted Context context,
+            @InternalWindowViewInflater StatusBarWindowViewInflater statusBarWindowViewInflater,
+            @Assisted ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
             IWindowManager iWindowManager,
             StatusBarContentInsetsProvider contentInsetsProvider,
             FragmentService fragmentService,
-            @Main Resources resources,
             Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider) {
         mContext = context;
         mWindowManager = viewCaptureAwareWindowManager;
         mIWindowManager = iWindowManager;
         mContentInsetsProvider = contentInsetsProvider;
-        mStatusBarWindowView = statusBarWindowView;
+        mStatusBarWindowView = statusBarWindowViewInflater.inflate(context);
         mFragmentService = fragmentService;
         mLaunchAnimationContainer = mStatusBarWindowView.findViewById(
                 R.id.status_bar_launch_animation_container);
@@ -354,4 +352,13 @@
             mLpChanged.forciblyShownTypes &= ~WindowInsets.Type.statusBars();
         }
     }
+
+    @AssistedFactory
+    public interface Factory {
+        /** Creates a new instance. */
+        StatusBarWindowControllerImpl create(
+                Context context,
+                ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt
index 1c7debc..ebfbac7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt
@@ -1,47 +1,36 @@
 package com.android.systemui.statusbar.window
 
-import android.view.LayoutInflater
-import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
 import dagger.Module
-import dagger.Provides
 import javax.inject.Qualifier
 
 /** Module providing dependencies related to the status bar window. */
 @Module
 abstract class StatusBarWindowModule {
-    /**
-     * Provides a [StatusBarWindowView].
-     *
-     * Only [StatusBarWindowController] should inject the view.
-     */
-    @Module
-    companion object {
-        @JvmStatic
-        @Provides
-        @SysUISingleton
-        @InternalWindowView
-        fun providesStatusBarWindowView(layoutInflater: LayoutInflater): StatusBarWindowView {
-            return layoutInflater.inflate(
-                R.layout.super_status_bar,
-                /* root= */null
-            ) as StatusBarWindowView?
-                ?: throw IllegalStateException(
-                    "R.layout.super_status_bar could not be properly inflated"
-                )
-        }
-    }
 
     /**
-     * We want [StatusBarWindowView] to be provided to [StatusBarWindowController]'s constructor via
-     * dagger so that we can provide a fake window view when testing the controller. However, we wan
-     * want *only* the controller to be able to inject the window view.
+     * Binds a [StatusBarWindowViewInflater].
      *
-     * This protected qualifier annotation achieves this. [StatusBarWindowView] can only be injected
-     * if it's annotated with [InternalWindowView], and only classes inside this [statusbar.window]
-     * package can access the annotation.
+     * Only [StatusBarWindowControllerImpl] should inject it.
+     */
+    @Binds
+    @SysUISingleton
+    @InternalWindowViewInflater
+    abstract fun providesStatusBarWindowViewInflater(
+        inflaterImpl: StatusBarWindowViewInflaterImpl
+    ): StatusBarWindowViewInflater
+
+    /**
+     * We want [StatusBarWindowViewInflater] to be provided to [StatusBarWindowControllerImpl]'s
+     * constructor via dagger so that we can provide a fake window view when testing the controller.
+     * However, we wan want *only* the controller to be able to inject the window view.
+     *
+     * This protected qualifier annotation achieves this. [StatusBarWindowViewInflater] can only be
+     * injected if it's annotated with [InternalWindowViewInflater], and only classes inside this
+     * [statusbar.window] package can access the annotation.
      */
     @Retention(AnnotationRetention.BINARY)
     @Qualifier
-    protected annotation class InternalWindowView
+    protected annotation class InternalWindowViewInflater
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowViewInflater.kt
new file mode 100644
index 0000000..f030a4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowViewInflater.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.content.Context
+import android.view.LayoutInflater
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * Inflates a [StatusBarWindowView]. Exists so that it can be injected into
+ * [StatusBarWindowControllerImpl] and be swapped for a fake implementation in tests.
+ */
+interface StatusBarWindowViewInflater {
+    fun inflate(context: Context): StatusBarWindowView
+}
+
+class StatusBarWindowViewInflaterImpl @Inject constructor() : StatusBarWindowViewInflater {
+
+    override fun inflate(context: Context): StatusBarWindowView {
+        val layoutInflater = LayoutInflater.from(context)
+        return layoutInflater.inflate(R.layout.super_status_bar, /* root= */ null)
+            as StatusBarWindowView?
+            ?: throw IllegalStateException(
+                "R.layout.super_status_bar could not be properly inflated"
+            )
+    }
+}
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/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index d3e20c6..53f0800 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -73,6 +73,7 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.media.InputMediaDevice;
+import com.android.settingslib.media.InputRouteManager;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.SysuiTestCase;
@@ -100,6 +101,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @SmallTest
@@ -115,6 +117,10 @@
     private static final String TEST_SONG = "test_song";
     private static final String TEST_SESSION_ID = "test_session_id";
     private static final String TEST_SESSION_NAME = "test_session_name";
+    private static final int MAX_VOLUME = 1;
+    private static final int CURRENT_VOLUME = 0;
+    private static final boolean VOLUME_FIXED_TRUE = true;
+
     @Mock
     private DialogTransitionAnimator mDialogTransitionAnimator;
     @Mock
@@ -181,6 +187,7 @@
     private String mPackageName = null;
     private MediaSwitchingController mMediaSwitchingController;
     private LocalMediaManager mLocalMediaManager;
+    private InputRouteManager mInputRouteManager;
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
     private List<NearbyDevice> mNearbyDevices = new ArrayList<>();
@@ -228,6 +235,10 @@
         mLocalMediaManager = spy(mMediaSwitchingController.mLocalMediaManager);
         when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
         mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
+        mMediaSwitchingController.mInputRouteManager =
+                new InputRouteManager(mContext, mAudioManager);
+        mInputRouteManager = spy(mMediaSwitchingController.mInputRouteManager);
+        mMediaSwitchingController.mInputRouteManager = mInputRouteManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
         builder.setTitle(TEST_SONG);
         builder.setSubtitle(TEST_ARTIST);
@@ -545,9 +556,6 @@
         // Output devices have changed.
         mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
-        final int MAX_VOLUME = 1;
-        final int CURRENT_VOLUME = 0;
-        final boolean IS_VOLUME_FIXED = true;
         final MediaDevice mediaDevice3 =
                 InputMediaDevice.create(
                         mContext,
@@ -555,7 +563,7 @@
                         AudioDeviceInfo.TYPE_BUILTIN_MIC,
                         MAX_VOLUME,
                         CURRENT_VOLUME,
-                        IS_VOLUME_FIXED);
+                        VOLUME_FIXED_TRUE);
         final MediaDevice mediaDevice4 =
                 InputMediaDevice.create(
                         mContext,
@@ -563,7 +571,7 @@
                         AudioDeviceInfo.TYPE_WIRED_HEADSET,
                         MAX_VOLUME,
                         CURRENT_VOLUME,
-                        IS_VOLUME_FIXED);
+                        VOLUME_FIXED_TRUE);
         final List<MediaDevice> inputDevices = new ArrayList<>();
         inputDevices.add(mediaDevice3);
         inputDevices.add(mediaDevice4);
@@ -1312,4 +1320,23 @@
 
         verify(mCallback).dismissDialog();
     }
+
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getSelectedMediaDevice() {
+        // Mock MediaDevice since none of the output media device constructor is publicly available
+        // outside of SettingsLib package.
+        final MediaDevice selectedOutputMediaDevice = mock(MediaDevice.class);
+        doReturn(Collections.singletonList(selectedOutputMediaDevice))
+                .when(mLocalMediaManager)
+                .getSelectedMediaDevice();
+
+        // Mock selected input media device.
+        final MediaDevice selectedInputMediaDevice = mock(MediaDevice.class);
+        doReturn(selectedInputMediaDevice).when(mInputRouteManager).getSelectedInputDevice();
+
+        List<MediaDevice> selectedMediaDevices = mMediaSwitchingController.getSelectedMediaDevice();
+        assertThat(selectedMediaDevices)
+                .containsExactly(selectedOutputMediaDevice, selectedInputMediaDevice);
+    }
 }
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/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
index ee36cad..de4bbec 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -84,7 +84,7 @@
         if (devices.containsKey(deviceId)) {
             return
         }
-        addPhysicalKeyboard(deviceId, enabled)
+        addPhysicalKeyboard(deviceId, enabled = enabled)
     }
 
     fun registerInputDeviceListener(listener: InputDeviceListener) {
@@ -92,9 +92,15 @@
         inputDeviceListener = listener
     }
 
-    fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
+    fun addPhysicalKeyboard(
+        id: Int,
+        vendorId: Int = 0,
+        productId: Int = 0,
+        isFullKeyboard: Boolean = true,
+        enabled: Boolean = true
+    ) {
         check(id > 0) { "Physical keyboard ids have to be > 0" }
-        addKeyboard(id, enabled)
+        addKeyboard(id, vendorId, productId, isFullKeyboard, enabled)
     }
 
     fun removeKeysFromKeyboard(deviceId: Int, vararg keyCodes: Int) {
@@ -102,20 +108,38 @@
         supportedKeyCodesByDeviceId[deviceId]!!.removeAll(keyCodes.asList())
     }
 
-    private fun addKeyboard(id: Int, enabled: Boolean = true) {
-        devices[id] =
+    private fun addKeyboard(
+        id: Int,
+        vendorId: Int = 0,
+        productId: Int = 0,
+        isFullKeyboard: Boolean = true,
+        enabled: Boolean = true
+    ) {
+        val keyboardType =
+            if (isFullKeyboard) InputDevice.KEYBOARD_TYPE_ALPHABETIC
+            else InputDevice.KEYBOARD_TYPE_NON_ALPHABETIC
+        // VendorId and productId are set to 0 if not specified, which is the same as the default
+        // values used in InputDevice.Builder
+        val builder =
             InputDevice.Builder()
                 .setId(id)
-                .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+                .setVendorId(vendorId)
+                .setProductId(productId)
+                .setKeyboardType(keyboardType)
                 .setSources(InputDevice.SOURCE_KEYBOARD)
                 .setEnabled(enabled)
                 .setKeyCharacterMap(keyCharacterMap)
-                .build()
+        devices[id] = builder.build()
+        inputDeviceListener?.onInputDeviceAdded(id)
         supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet()
     }
 
-    fun addDevice(id: Int, sources: Int) {
-        devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+    fun addDevice(id: Int, sources: Int, isNotFound: Boolean = false) {
+        // there's not way of differentiate device connection vs registry in current implementation.
+        // If the device isNotFound, it means that we connect an unregistered device.
+        if (!isNotFound) {
+            devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+        }
         inputDeviceListener?.onInputDeviceAdded(id)
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
index 6e650a3..77afa79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
 import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.controls.shared.mediaLogger
 import com.android.systemui.media.controls.util.mediaInstanceId
 import com.android.systemui.media.mediaOutputDialogManager
 import com.android.systemui.plugins.activityStarter
@@ -39,5 +40,6 @@
             lockscreenUserManager = notificationLockscreenUserManager,
             mediaOutputDialogManager = mediaOutputDialogManager,
             broadcastDialogController = mockBroadcastDialogController,
+            mediaLogger = mediaLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
index e490b75..9ea660f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
 import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.controls.shared.mediaLogger
 import com.android.systemui.media.mediaOutputDialogManager
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.statusbar.notificationLockscreenUserManager
@@ -42,6 +43,7 @@
                     lockscreenUserManager = notificationLockscreenUserManager,
                     mediaOutputDialogManager = mediaOutputDialogManager,
                     broadcastDialogController = mockBroadcastDialogController,
+                    mediaLogger = mediaLogger,
                 )
             }
         }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index 8c6f50e..e29b6e4 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -145,13 +145,25 @@
         ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap =
                 getRemovedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
 
-        Set<AppSearchSchema> appRuntimeMetadataSchemas =
-                getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet());
-        appRuntimeMetadataSchemas.add(
-                AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema());
+        if (!staticPackageToFunctionMap.keySet().equals(runtimePackageToFunctionMap.keySet())) {
+            // Drop removed packages from removedFunctionsDiffMap, as setSchema() deletes them
+            ArraySet<String> removedPackages =
+                    getRemovedPackages(
+                            staticPackageToFunctionMap.keySet(), removedFunctionsDiffMap.keySet());
+            for (String packageName : removedPackages) {
+                removedFunctionsDiffMap.remove(packageName);
+            }
+            Set<AppSearchSchema> appRuntimeMetadataSchemas =
+                    getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet());
+            appRuntimeMetadataSchemas.add(
+                    AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema());
+            SetSchemaRequest addSetSchemaRequest =
+                    buildSetSchemaRequestForRuntimeMetadataSchemas(
+                            mPackageManager, appRuntimeMetadataSchemas);
+            Objects.requireNonNull(
+                    runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
+        }
 
-        // Operation order matters here. i.e. remove -> setSchema -> add. Otherwise we would
-        // encounter an error trying to delete a document with no existing schema.
         if (!removedFunctionsDiffMap.isEmpty()) {
             RemoveByDocumentIdRequest removeByDocumentIdRequest =
                     buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap);
@@ -164,12 +176,6 @@
         }
 
         if (!addedFunctionsDiffMap.isEmpty()) {
-            // TODO(b/357551503): only set schema on package diff
-            SetSchemaRequest addSetSchemaRequest =
-                    buildSetSchemaRequestForRuntimeMetadataSchemas(
-                            mPackageManager, appRuntimeMetadataSchemas);
-            Objects.requireNonNull(
-                    runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
             PutDocumentsRequest putDocumentsRequest =
                     buildPutRuntimeMetadataRequest(addedFunctionsDiffMap);
             AppSearchBatchResult<String, Void> putDocumentBatchResult =
@@ -276,6 +282,30 @@
     }
 
     /**
+     * This method returns a set of packages that are in the removed function packages but not in
+     * the all existing static packages.
+     *
+     * @param allExistingStaticPackages A set of all existing static metadata packages.
+     * @param removedFunctionPackages A set of all removed function packages.
+     * @return A set of packages that are in the removed function packages but not in the all
+     *     existing static packages.
+     */
+    @NonNull
+    private static ArraySet<String> getRemovedPackages(
+            @NonNull Set<String> allExistingStaticPackages,
+            @NonNull Set<String> removedFunctionPackages) {
+        ArraySet<String> removedPackages = new ArraySet<>();
+
+        for (String packageName : removedFunctionPackages) {
+            if (!allExistingStaticPackages.contains(packageName)) {
+                removedPackages.add(packageName);
+            }
+        }
+
+        return removedPackages;
+    }
+
+    /**
      * This method returns a map of package names to a set of function ids that are in the static
      * metadata but not in the runtime metadata.
      *
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index b109472..2fa0e0d 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -720,6 +720,9 @@
 
         void handleInlineSuggestionRequest(InlineSuggestionsRequest inlineSuggestionsRequest,
                 ViewState viewState) {
+            if (sVerbose) {
+                Slog.v(TAG, "handleInlineSuggestionRequest(): inline suggestion request received");
+            }
             synchronized (mLock) {
                 if (!mWaitForInlineRequest || mPendingInlineSuggestionsRequest != null) {
                     return;
@@ -734,15 +737,27 @@
         @GuardedBy("mLock")
         void maybeRequestFillLocked() {
             if (mPendingFillRequest == null) {
+                if (sVerbose) {
+                        Slog.v(TAG, "maybeRequestFillLocked(): cancelling calling fill request "
+                            + "due to empty pending fill request");
+                }
                 return;
             }
             mFieldClassificationIdSnapshot = sIdCounterForPcc.get();
 
             if (mWaitForInlineRequest) {
                 if (mPendingInlineSuggestionsRequest == null) {
+                    if (sVerbose) {
+                        Slog.v(TAG, "maybeRequestFillLocked(): cancelling calling fill request "
+                            + "due to waiting for inline request and pending inline request is "
+                            + "currently empty");
+                    }
                     return;
                 }
-
+                if (sVerbose) {
+                    Slog.v(TAG, "maybeRequestFillLocked(): adding inline request to pending "
+                        + "fill request");
+                }
                 mPendingFillRequest = new FillRequest(mPendingFillRequest.getId(),
                         mPendingFillRequest.getFillContexts(),
                         mPendingFillRequest.getHints(),
@@ -750,8 +765,17 @@
                         mPendingFillRequest.getFlags(),
                         mPendingInlineSuggestionsRequest,
                         mPendingFillRequest.getDelayedFillIntentSender());
+            } else {
+                if (sVerbose) {
+                    Slog.v(TAG, "maybeRequestFillLocked(): not adding inline request to pending "
+                        + "fill request");
+                }
             }
+
             mLastFillRequest = mPendingFillRequest;
+            if (sVerbose) {
+                Slog.v(TAG, "maybeRequestFillLocked(): sending fill request");
+            }
             if (shouldRequestSecondaryProvider(mPendingFillRequest.getFlags())
                     && mSecondaryProviderHandler != null) {
                 Slog.v(TAG, "Requesting fill response to secondary provider.");
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 6657c1c..59dea09 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -160,6 +160,7 @@
     private int mLastChargeCounter;
     private int mLastBatteryCycleCount;
     private int mLastChargingState;
+    private int mLastBatteryCapacityLevel;
     /**
      * The last seen charging policy. This requires the
      * {@link android.Manifest.permission#BATTERY_STATS} permission and should therefore not be
@@ -609,7 +610,8 @@
                         || mHealthInfo.batteryChargeCounterUah != mLastChargeCounter
                         || mInvalidCharger != mLastInvalidCharger
                         || mHealthInfo.batteryCycleCount != mLastBatteryCycleCount
-                        || mHealthInfo.chargingState != mLastChargingState)) {
+                        || mHealthInfo.chargingState != mLastChargingState
+                        || mHealthInfo.batteryCapacityLevel != mLastBatteryCapacityLevel)) {
 
             if (mPlugType != mLastPlugType) {
                 if (mLastPlugType == BATTERY_PLUGGED_NONE) {
@@ -829,6 +831,7 @@
             mLastInvalidCharger = mInvalidCharger;
             mLastBatteryCycleCount = mHealthInfo.batteryCycleCount;
             mLastChargingState = mHealthInfo.chargingState;
+            mLastBatteryCapacityLevel = mHealthInfo.batteryCapacityLevel;
         }
     }
 
@@ -862,6 +865,7 @@
         intent.putExtra(BatteryManager.EXTRA_CHARGE_COUNTER, mHealthInfo.batteryChargeCounterUah);
         intent.putExtra(BatteryManager.EXTRA_CYCLE_COUNT, mHealthInfo.batteryCycleCount);
         intent.putExtra(BatteryManager.EXTRA_CHARGING_STATUS, mHealthInfo.chargingState);
+        intent.putExtra(BatteryManager.EXTRA_CAPACITY_LEVEL, mHealthInfo.batteryCapacityLevel);
         if (DEBUG) {
             Slog.d(TAG, "Sending ACTION_BATTERY_CHANGED. scale:" + BATTERY_SCALE
                     + ", info:" + mHealthInfo.toString());
@@ -964,6 +968,7 @@
         event.putLong(BatteryManager.EXTRA_EVENT_TIMESTAMP, now);
         event.putInt(BatteryManager.EXTRA_CYCLE_COUNT, mHealthInfo.batteryCycleCount);
         event.putInt(BatteryManager.EXTRA_CHARGING_STATUS, mHealthInfo.chargingState);
+        event.putInt(BatteryManager.EXTRA_CAPACITY_LEVEL, mHealthInfo.batteryCapacityLevel);
 
         boolean queueWasEmpty = mBatteryLevelsEventQueue.isEmpty();
         mBatteryLevelsEventQueue.add(event);
@@ -1401,6 +1406,7 @@
                 pw.println("  technology: " + mHealthInfo.batteryTechnology);
                 pw.println("  Charging state: " + mHealthInfo.chargingState);
                 pw.println("  Charging policy: " + mHealthInfo.chargingPolicy);
+                pw.println("  Capacity level: " + mHealthInfo.batteryCapacityLevel);
             } else {
                 Shell shell = new Shell();
                 shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null));
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 1c13ad5..f32031de 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -32,11 +32,11 @@
 import static android.app.UiModeManager.PROJECTION_TYPE_NONE;
 import static android.os.UserHandle.USER_SYSTEM;
 import static android.os.UserHandle.getCallingUserId;
-import static android.os.UserManager.isVisibleBackgroundUsersEnabled;
 import static android.provider.Settings.Secure.CONTRAST_LEVEL;
 import static android.util.TimeUtils.isTimeBetween;
 
 import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
+import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled;
 
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -100,7 +100,6 @@
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.DumpUtils;
-import com.android.server.pm.UserManagerService;
 import com.android.server.twilight.TwilightListener;
 import com.android.server.twilight.TwilightManager;
 import com.android.server.twilight.TwilightState;
@@ -850,7 +849,7 @@
             }
 
             final int user = UserHandle.getCallingUserId();
-            enforceValidCallingUser(user);
+            enforceCurrentUserIfVisibleBackgroundEnabled(user);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -914,7 +913,7 @@
                 @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) {
             setAttentionModeThemeOverlay_enforcePermission();
 
-            enforceValidCallingUser(UserHandle.getCallingUserId());
+            enforceCurrentUserIfVisibleBackgroundEnabled(UserHandle.getCallingUserId());
 
             synchronized (mLock) {
                 if (mAttentionModeThemeOverlay != attentionModeThemeOverlayType) {
@@ -1005,7 +1004,7 @@
                 return false;
             }
             final int user = Binder.getCallingUserHandle().getIdentifier();
-            enforceValidCallingUser(user);
+            enforceCurrentUserIfVisibleBackgroundEnabled(user);
 
             if (user != mCurrentUser && getContext().checkCallingOrSelfPermission(
                     android.Manifest.permission.INTERACT_ACROSS_USERS)
@@ -1064,7 +1063,7 @@
                 return;
             }
             final int user = UserHandle.getCallingUserId();
-            enforceValidCallingUser(user);
+            enforceCurrentUserIfVisibleBackgroundEnabled(user);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -1094,7 +1093,7 @@
                 return;
             }
             final int user = UserHandle.getCallingUserId();
-            enforceValidCallingUser(user);
+            enforceCurrentUserIfVisibleBackgroundEnabled(user);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -1116,7 +1115,7 @@
             assertLegit(callingPackage);
             assertSingleProjectionType(projectionType);
             enforceProjectionTypePermissions(projectionType);
-            enforceValidCallingUser(getCallingUserId());
+            enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId());
 
             synchronized (mLock) {
                 if (mProjectionHolders == null) {
@@ -1162,7 +1161,7 @@
             assertLegit(callingPackage);
             assertSingleProjectionType(projectionType);
             enforceProjectionTypePermissions(projectionType);
-            enforceValidCallingUser(getCallingUserId());
+            enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId());
 
             return releaseProjectionUnchecked(projectionType, callingPackage);
         }
@@ -1204,7 +1203,7 @@
                 return;
             }
 
-            enforceValidCallingUser(getCallingUserId());
+            enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId());
 
             synchronized (mLock) {
                 if (mProjectionListeners == null) {
@@ -1253,32 +1252,6 @@
         }
     };
 
-    // This method validates whether calling user is valid in visible background users
-    // feature. Valid user is the current user or the system or in the same profile group as
-    // the current user.
-    private void enforceValidCallingUser(int userId) {
-        if (!isVisibleBackgroundUsersEnabled()) {
-            return;
-        }
-        if (LOG) {
-            Slog.d(TAG, "enforceValidCallingUser: userId=" + userId
-                    + " isSystemUser=" + (userId == USER_SYSTEM) + " current user=" + mCurrentUser
-                    + " callingPid=" + Binder.getCallingPid()
-                    + " callingUid=" + mInjector.getCallingUid());
-        }
-        long ident = Binder.clearCallingIdentity();
-        try {
-            if (userId != USER_SYSTEM && userId != mCurrentUser
-                    && !UserManagerService.getInstance().isSameProfileGroup(userId, mCurrentUser)) {
-                throw new SecurityException(
-                        "Calling user is not valid for level-1 compatibility in MUMD. "
-                                + "callingUserId=" + userId + " currentUserId=" + mCurrentUser);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
     private void enforceProjectionTypePermissions(@UiModeManager.ProjectionType int p) {
         if ((p & PROJECTION_TYPE_AUTOMOTIVE) != 0) {
             getContext().enforceCallingPermission(
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 54a7410..871c320 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -714,12 +714,14 @@
     /**
      * Map userId to its companion app uids.
      */
+    @GuardedBy("mCompanionAppUidsMap")
     private final Map<Integer, Set<Integer>> mCompanionAppUidsMap = new ArrayMap<>();
 
     /**
      * The profile owner UIDs.
      */
-    private ArraySet<Integer> mProfileOwnerUids = null;
+    @GuardedBy("mProfileOwnerUids")
+    private final ArraySet<Integer> mProfileOwnerUids = new ArraySet<>();
 
     final UserController mUserController;
     @VisibleForTesting
@@ -17535,32 +17537,35 @@
 
         @Override
         public void setProfileOwnerUid(ArraySet<Integer> profileOwnerUids) {
-            synchronized (ActivityManagerService.this) {
-                mProfileOwnerUids = profileOwnerUids;
+            synchronized (mProfileOwnerUids) {
+                mProfileOwnerUids.clear();
+                mProfileOwnerUids.addAll(profileOwnerUids);
             }
         }
 
         @Override
         public boolean isProfileOwner(int uid) {
-            synchronized (ActivityManagerService.this) {
-                return mProfileOwnerUids != null && mProfileOwnerUids.indexOf(uid) >= 0;
+            synchronized (mProfileOwnerUids) {
+                return mProfileOwnerUids.indexOf(uid) >= 0;
             }
         }
 
         @Override
         public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) {
-            synchronized (ActivityManagerService.this) {
+            synchronized (mCompanionAppUidsMap) {
                 mCompanionAppUidsMap.put(userId, companionAppUids);
             }
         }
 
         @Override
         public boolean isAssociatedCompanionApp(int userId, int uid) {
-            final Set<Integer> allUids = mCompanionAppUidsMap.get(userId);
-            if (allUids == null) {
-                return false;
+            synchronized (mCompanionAppUidsMap) {
+                final Set<Integer> allUids = mCompanionAppUidsMap.get(userId);
+                if (allUids == null) {
+                    return false;
+                }
+                return allUids.contains(uid);
             }
-            return allUids.contains(uid);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/am/CoreSettingsObserver.java b/services/core/java/com/android/server/am/CoreSettingsObserver.java
index 6bb56c9..e885c14 100644
--- a/services/core/java/com/android/server/am/CoreSettingsObserver.java
+++ b/services/core/java/com/android/server/am/CoreSettingsObserver.java
@@ -79,6 +79,7 @@
         sSecureSettingToTypeMap.put(Settings.Secure.MULTI_PRESS_TIMEOUT, int.class);
         sSecureSettingToTypeMap.put(Settings.Secure.KEY_REPEAT_TIMEOUT_MS, int.class);
         sSecureSettingToTypeMap.put(Settings.Secure.KEY_REPEAT_DELAY_MS, int.class);
+        sSecureSettingToTypeMap.put(Settings.Secure.KEY_REPEAT_ENABLED, int.class);
         sSecureSettingToTypeMap.put(Settings.Secure.STYLUS_POINTER_ICON_ENABLED, int.class);
         // add other secure settings here...
 
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index b186eaa..262c76e 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -156,6 +156,9 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 
@@ -223,18 +226,9 @@
     private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000;
 
     /**
-     * Amount of time waited for
-     * {@link ActivityTaskManagerInternal.ScreenObserver#onKeyguardStateChanged} callbacks to be
-     * called after calling {@link WindowManagerService#lockDeviceNow}.
-     * Otherwise, we should throw a {@link RuntimeException} and never dismiss the
-     * {@link UserSwitchingDialog}.
-     */
-    static final int SHOW_KEYGUARD_TIMEOUT_MS = 20 * 1000;
-
-    /**
      * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
      * called after dismissing the keyguard.
-     * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog}}
+     * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()}
      * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
      */
     private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
@@ -1986,10 +1980,18 @@
                 // it should be moved outside, but for now it's not as there are many calls to
                 // external components here afterwards
                 updateProfileRelatedCaches();
+                dispatchOnBeforeUserSwitching(userId);
                 mInjector.getWindowManager().setCurrentUser(userId);
                 mInjector.reportCurWakefulnessUsageEvent();
+                // Once the internal notion of the active user has switched, we lock the device
+                // with the option to show the user switcher on the keyguard.
                 if (userSwitchUiEnabled) {
                     mInjector.getWindowManager().setSwitchingUser(true);
+                    // Only lock if the user has a secure keyguard PIN/Pattern/Pwd
+                    if (mInjector.getKeyguardManager().isDeviceSecure(userId)) {
+                        // Make sure the device is locked before moving on with the user switch
+                        mInjector.lockDeviceNowAndWaitForKeyguardShown();
+                    }
                 }
 
             } else {
@@ -2284,6 +2286,25 @@
         mUserSwitchObservers.finishBroadcast();
     }
 
+    private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId) {
+        final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+        t.traceBegin("dispatchOnBeforeUserSwitching-" + newUserId);
+        final int observerCount = mUserSwitchObservers.beginBroadcast();
+        for (int i = 0; i < observerCount; i++) {
+            final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
+            t.traceBegin("onBeforeUserSwitching-" + name);
+            try {
+                mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
+            } catch (RemoteException e) {
+                // Ignore
+            } finally {
+                t.traceEnd();
+            }
+        }
+        mUserSwitchObservers.finishBroadcast();
+        t.traceEnd();
+    }
+
     /** Called on handler thread */
     @VisibleForTesting
     void dispatchUserSwitchComplete(@UserIdInt int oldUserId, @UserIdInt int newUserId) {
@@ -2499,17 +2520,6 @@
 
         final int observerCount = mUserSwitchObservers.beginBroadcast();
         if (observerCount > 0) {
-            for (int i = 0; i < observerCount; i++) {
-                final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
-                t.traceBegin("onBeforeUserSwitching-" + name);
-                try {
-                    mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
-                } catch (RemoteException e) {
-                    // Ignore
-                } finally {
-                    t.traceEnd();
-                }
-            }
             final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
             synchronized (mLock) {
                 uss.switching = true;
@@ -2606,56 +2616,34 @@
 
     @VisibleForTesting
     void completeUserSwitch(int oldUserId, int newUserId) {
-        final Runnable sendUserSwitchCompleteMessage = () -> {
-            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
-            mHandler.sendMessage(mHandler.obtainMessage(
-                    REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
-        };
-        if (isUserSwitchUiEnabled()) {
-            if (mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
-                this.showKeyguard(() -> dismissUserSwitchDialog(sendUserSwitchCompleteMessage));
-            } else {
-                this.dismissKeyguard(() -> dismissUserSwitchDialog(sendUserSwitchCompleteMessage));
-            }
+        final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
+        // serialize each conditional step
+        await(
+                // STEP 1 - If there is no challenge set, dismiss the keyguard right away
+                isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId),
+                mInjector::dismissKeyguard,
+                () -> await(
+                        // STEP 2 - If user switch ui was enabled, dismiss user switch dialog
+                        isUserSwitchUiEnabled,
+                        this::dismissUserSwitchDialog,
+                        () -> {
+                            // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast
+                            // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete
+                            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
+                            mHandler.sendMessage(mHandler.obtainMessage(
+                                    REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
+                        }
+                ));
+    }
+
+    private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) {
+        if (condition) {
+            conditionalStep.accept(nextStep);
         } else {
-            sendUserSwitchCompleteMessage.run();
+            nextStep.run();
         }
     }
 
-    protected void showKeyguard(Runnable runnable) {
-        runWithTimeout(mInjector::showKeyguard, SHOW_KEYGUARD_TIMEOUT_MS, runnable, () -> {
-            throw new RuntimeException(
-                    "Keyguard is not shown in " + SHOW_KEYGUARD_TIMEOUT_MS + " ms.");
-        }, "showKeyguard");
-    }
-
-    protected void dismissKeyguard(Runnable runnable) {
-        runWithTimeout(mInjector::dismissKeyguard, DISMISS_KEYGUARD_TIMEOUT_MS, runnable, runnable,
-                "dismissKeyguard");
-    }
-
-    private void runWithTimeout(Consumer<Runnable> task, int timeoutMs, Runnable onSuccess,
-            Runnable onTimeout, String traceMsg) {
-        final AtomicInteger state = new AtomicInteger(0); // state = 0 (RUNNING)
-
-        asyncTraceBegin(traceMsg, 0);
-
-        mHandler.postDelayed(() -> {
-            if (state.compareAndSet(0, 1)) { // state = 1 (TIMEOUT)
-                asyncTraceEnd(traceMsg, 0);
-                Slogf.w(TAG, "Timeout: %s did not finish in %d ms", traceMsg, timeoutMs);
-                onTimeout.run();
-            }
-        }, timeoutMs);
-
-        task.accept(() -> {
-            if (state.compareAndSet(0, 2)) { // state = 2 (SUCCESS)
-                asyncTraceEnd(traceMsg, 0);
-                onSuccess.run();
-            }
-        });
-    }
-
     private void moveUserToForeground(UserState uss, int newUserId) {
         boolean homeInFront = mInjector.taskSupervisorSwitchUser(newUserId, uss);
         if (homeInFront) {
@@ -4100,45 +4088,29 @@
             return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
         }
 
-        protected void showKeyguard(Runnable runnable) {
-            if (getWindowManager().isKeyguardLocked()) {
-                runnable.run();
-                return;
-            }
-            getActivityTaskManagerInternal().registerScreenObserver(
-                    new ActivityTaskManagerInternal.ScreenObserver() {
-                        @Override
-                        public void onAwakeStateChanged(boolean isAwake) {
-
-                        }
-
-                        @Override
-                        public void onKeyguardStateChanged(boolean isShowing) {
-                            if (isShowing) {
-                                getActivityTaskManagerInternal().unregisterScreenObserver(this);
-                                runnable.run();
-                            }
-                        }
-                    }
-            );
-            getWindowManager().lockDeviceNow();
-        }
-
         protected void dismissKeyguard(Runnable runnable) {
+            final AtomicBoolean isFirst = new AtomicBoolean(true);
+            final Runnable runOnce = () -> {
+                if (isFirst.getAndSet(false)) {
+                    runnable.run();
+                }
+            };
+
+            mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS);
             getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() {
                 @Override
                 public void onDismissError() throws RemoteException {
-                    runnable.run();
+                    mHandler.post(runOnce);
                 }
 
                 @Override
                 public void onDismissSucceeded() throws RemoteException {
-                    runnable.run();
+                    mHandler.post(runOnce);
                 }
 
                 @Override
                 public void onDismissCancelled() throws RemoteException {
-                    runnable.run();
+                    mHandler.post(runOnce);
                 }
             }, /* message= */ null);
         }
@@ -4164,5 +4136,43 @@
         void onSystemUserVisibilityChanged(boolean visible) {
             getUserManagerInternal().onSystemUserVisibilityChanged(visible);
         }
+
+        void lockDeviceNowAndWaitForKeyguardShown() {
+            if (getWindowManager().isKeyguardLocked()) {
+                return;
+            }
+
+            final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+            t.traceBegin("lockDeviceNowAndWaitForKeyguardShown");
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            ActivityTaskManagerInternal.ScreenObserver screenObserver =
+                    new ActivityTaskManagerInternal.ScreenObserver() {
+                        @Override
+                        public void onAwakeStateChanged(boolean isAwake) {
+
+                        }
+
+                        @Override
+                        public void onKeyguardStateChanged(boolean isShowing) {
+                            if (isShowing) {
+                                latch.countDown();
+                            }
+                        }
+                    };
+
+            getActivityTaskManagerInternal().registerScreenObserver(screenObserver);
+            getWindowManager().lockDeviceNow();
+            try {
+                if (!latch.await(20, TimeUnit.SECONDS)) {
+                    throw new RuntimeException("Keyguard is not shown in 20 seconds");
+                }
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } finally {
+                getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver);
+                t.traceEnd();
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 835fb72..d70bd8b 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -94,6 +94,8 @@
                         (reason) -> updateKeyRepeatInfo()),
                 Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_DELAY_MS),
                         (reason) -> updateKeyRepeatInfo()),
+                Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_ENABLED),
+                        (reason) -> updateKeyRepeatInfo()),
                 Map.entry(Settings.System.getUriFor(Settings.System.SHOW_ROTARY_INPUT),
                         (reason) -> updateShowRotaryInput()),
                 Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS),
@@ -230,6 +232,11 @@
     }
 
     private void updateKeyRepeatInfo() {
+        // Key repeat is enabled by default
+        final boolean keyRepeatEnabled = Settings.Secure.getIntForUser(
+                mContext.getContentResolver(), Settings.Secure.KEY_REPEAT_ENABLED, 1,
+                UserHandle.USER_CURRENT) != 0;
+
         // Use ViewConfiguration getters only as fallbacks because they may return stale values.
         final int timeoutMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                 Settings.Secure.KEY_REPEAT_TIMEOUT_MS, ViewConfiguration.getKeyRepeatTimeout(),
@@ -237,7 +244,7 @@
         final int delayMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                 Settings.Secure.KEY_REPEAT_DELAY_MS, ViewConfiguration.getKeyRepeatDelay(),
                 UserHandle.USER_CURRENT);
-        mNative.setKeyRepeatConfiguration(timeoutMs, delayMs);
+        mNative.setKeyRepeatConfiguration(timeoutMs, delayMs, keyRepeatEnabled);
     }
 
     private void updateMaximumObscuringOpacityForTouch() {
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 1e7c97f9..5dd461d 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -212,7 +212,7 @@
 
     void setMotionClassifierEnabled(boolean enabled);
 
-    void setKeyRepeatConfiguration(int timeoutMs, int delayMs);
+    void setKeyRepeatConfiguration(int timeoutMs, int delayMs, boolean keyRepeatEnabled);
 
     InputSensorInfo[] getSensorList(int deviceId);
 
@@ -509,7 +509,8 @@
         public native void setMotionClassifierEnabled(boolean enabled);
 
         @Override
-        public native void setKeyRepeatConfiguration(int timeoutMs, int delayMs);
+        public native void setKeyRepeatConfiguration(int timeoutMs, int delayMs,
+                boolean keyRepeatEnabled);
 
         @Override
         public native InputSensorInfo[] getSensorList(int deviceId);
diff --git a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
index 01c108b..494ea77 100644
--- a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
+++ b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
@@ -19,6 +19,7 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.content.Context;
+import android.location.flags.Flags;
 import android.os.Looper;
 
 import java.io.PrintWriter;
@@ -55,7 +56,7 @@
     static NetworkTimeHelper create(
             @NonNull Context context, @NonNull Looper looper,
             @NonNull InjectTimeCallback injectTimeCallback) {
-        if (USE_TIME_DETECTOR_IMPL) {
+        if (!Flags.useLegacyNtpTime()) {
             TimeDetectorNetworkTimeHelper.Environment environment =
                     new TimeDetectorNetworkTimeHelper.EnvironmentImpl(looper);
             return new TimeDetectorNetworkTimeHelper(environment, injectTimeCallback);
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/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 621c090..48d24f2 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -179,7 +179,8 @@
     /**
      * In order to record the keyguard, the MediaProjection package must be either:
      *   - a holder of RECORD_SENSITIVE_CONTENT permission, or
-     *   - be one of the bugreport whitelisted packages
+     *   - be one of the bugreport allowlisted packages, or
+     *   - hold the OP_PROJECT_MEDIA AppOp.
      */
     private boolean canCaptureKeyguard() {
         if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
@@ -194,6 +195,14 @@
                     == PackageManager.PERMISSION_GRANTED) {
                 return true;
             }
+            boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA,
+                    mProjectionGrant.uid,
+                    mProjectionGrant.packageName);
+            if (operationActive) {
+                // Some tools use media projection by granting the OP_PROJECT_MEDIA app
+                // op via a shell command. Those tools can be granted keyguard capture
+                return true;
+            }
             return SystemConfig.getInstance().getBugreportWhitelistedPackages()
                     .contains(mProjectionGrant.packageName);
         }
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index c75622c..21a6df2 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -208,6 +208,22 @@
       "name": "CtsUpdateOwnershipEnforcementTestCases"
     },
     {
+      "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+      "file_patterns": [
+        "core/java/.*Install.*",
+        "services/core/.*Install.*",
+        "services/core/java/com/android/server/pm/.*"
+      ],
+      "options":[
+          {
+              "exclude-annotation":"androidx.test.filters.FlakyTest"
+          },
+          {
+              "exclude-annotation":"org.junit.Ignore"
+          }
+      ]
+    },
+    {
       "name": "CtsPackageInstallerCUJInstallationTestCases",
       "file_patterns": [
         "core/java/.*Install.*",
@@ -224,6 +240,22 @@
       ]
     },
     {
+      "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+      "file_patterns": [
+        "core/java/.*Install.*",
+        "services/core/.*Install.*",
+        "services/core/java/com/android/server/pm/.*"
+      ],
+      "options":[
+          {
+              "exclude-annotation":"androidx.test.filters.FlakyTest"
+          },
+          {
+              "exclude-annotation":"org.junit.Ignore"
+          }
+      ]
+    },
+    {
       "name": "CtsPackageInstallerCUJUninstallationTestCases",
       "file_patterns": [
         "core/java/.*Install.*",
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 68026ea..e3d71e4 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -24,8 +24,11 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -78,6 +81,8 @@
 public final class RollbackPackageHealthObserver implements PackageHealthObserver {
     private static final String TAG = "RollbackPackageHealthObserver";
     private static final String NAME = "rollback-observer";
+    private static final String ACTION_NAME = RollbackPackageHealthObserver.class.getName();
+
     private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
             | ApplicationInfo.FLAG_SYSTEM;
 
@@ -596,12 +601,40 @@
             }
         };
 
-        final LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver(result -> {
-            mHandler.post(() -> onResult.accept(result));
-        });
+        if (Flags.refactorCrashrecovery()) {
+            // Define a BroadcastReceiver to handle the result
+            BroadcastReceiver rollbackReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent result) {
+                    mHandler.post(() -> onResult.accept(result));
+                }
+            };
 
-        rollbackManager.commitRollback(rollback.getRollbackId(),
-                Collections.singletonList(failedPackage), rollbackReceiver.getIntentSender());
+            // Register the BroadcastReceiver
+            mContext.registerReceiver(rollbackReceiver,
+                    new IntentFilter(ACTION_NAME),
+                    Context.RECEIVER_NOT_EXPORTED);
+
+            Intent intentReceiver = new Intent(ACTION_NAME);
+            intentReceiver.putExtra("rollbackId", rollback.getRollbackId());
+            intentReceiver.setPackage(mContext.getPackageName());
+
+            PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext,
+                    rollback.getRollbackId(),
+                    intentReceiver,
+                    PendingIntent.FLAG_MUTABLE);
+
+            rollbackManager.commitRollback(rollback.getRollbackId(),
+                    Collections.singletonList(failedPackage),
+                    rollbackPendingIntent.getIntentSender());
+        } else {
+            final LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver(result -> {
+                mHandler.post(() -> onResult.accept(result));
+            });
+
+            rollbackManager.commitRollback(rollback.getRollbackId(),
+                    Collections.singletonList(failedPackage), rollbackReceiver.getIntentSender());
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 3d5b273..6009b4a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -6218,7 +6218,8 @@
         public void onProcessRemoved(String name, int uid) {
             synchronized (mGlobalLockWithoutBoost) {
                 final WindowProcessController proc = mProcessNames.remove(name, uid);
-                if (proc != null && !mStartingProcessActivities.isEmpty()) {
+                if (proc != null && !proc.mHasEverAttached
+                        && !mStartingProcessActivities.isEmpty()) {
                     // Use a copy in case finishIfPossible changes the list indirectly.
                     final ArrayList<ActivityRecord> activities =
                             new ArrayList<>(mStartingProcessActivities);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 8f5612c..84072e2 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1841,6 +1841,7 @@
     }
 
     boolean attachApplication(WindowProcessController app) throws RemoteException {
+        app.mHasEverAttached = true;
         final ArrayList<ActivityRecord> activities = mService.mStartingProcessActivities;
         RemoteException remoteException = null;
         boolean hasActivityStarted = false;
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/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 30d6f0a..32fe303 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -204,6 +204,9 @@
     // Set to true when process was launched with a wrapper attached
     private volatile boolean mUsingWrapper;
 
+    /** Whether this process has ever completed ActivityThread#handleBindApplication. */
+    boolean mHasEverAttached;
+
     /** Non-null if this process may have a window. */
     @Nullable
     Session mWindowSession;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 155e73c..d2493c5 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2725,12 +2725,13 @@
 }
 
 static void nativeSetKeyRepeatConfiguration(JNIEnv* env, jobject nativeImplObj, jint timeoutMs,
-                                            jint delayMs) {
+                                            jint delayMs, jboolean keyRepeatEnabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getDispatcher().setKeyRepeatConfiguration(std::chrono::milliseconds(
                                                                              timeoutMs),
                                                                      std::chrono::milliseconds(
-                                                                             delayMs));
+                                                                             delayMs),
+                                                                     keyRepeatEnabled);
 }
 
 static jobject createInputSensorInfo(JNIEnv* env, jstring name, jstring vendor, jint version,
@@ -3029,7 +3030,7 @@
         {"setDisplayEligibilityForPointerCapture", "(IZ)V",
          (void*)nativeSetDisplayEligibilityForPointerCapture},
         {"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled},
-        {"setKeyRepeatConfiguration", "(II)V", (void*)nativeSetKeyRepeatConfiguration},
+        {"setKeyRepeatConfiguration", "(IIZ)V", (void*)nativeSetKeyRepeatConfiguration},
         {"getSensorList", "(I)[Landroid/hardware/input/InputSensorInfo;",
          (void*)nativeGetSensorList},
         {"getTouchpadHardwareProperties",
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ab459df..3b334ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -107,7 +107,7 @@
 import com.android.internal.os.RuntimeInit;
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.protolog.ProtoLog;
-import com.android.internal.protolog.ProtoLogConfigurationService;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.util.ConcurrentUtils;
 import com.android.internal.util.EmergencyAffordanceManager;
@@ -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";
@@ -1097,7 +1101,7 @@
         if (android.tracing.Flags.clientSideProtoLogging()) {
             t.traceBegin("StartProtoLogConfigurationService");
             ServiceManager.addService(
-                    Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationService());
+                    Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationServiceImpl());
             t.traceEnd();
         }
 
@@ -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/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index a25621a..390eb93 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -61,7 +61,6 @@
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doNothing;
@@ -95,7 +94,6 @@
 import android.os.Message;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.IStorageManager;
@@ -214,10 +212,7 @@
             doNothing().when(mInjector).activityManagerOnUserStopped(anyInt());
             doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt());
             doNothing().when(mInjector).taskSupervisorRemoveUser(anyInt());
-            doAnswer(invocation -> {
-                ((Runnable) invocation.getArgument(0)).run();
-                return null;
-            }).when(mInjector).showKeyguard(any());
+            doNothing().when(mInjector).lockDeviceNowAndWaitForKeyguardShown();
             mockIsUsersOnSecondaryDisplaysEnabled(false);
             // All UserController params are set to default.
 
@@ -432,6 +427,7 @@
         mUserController.registerUserSwitchObserver(observer, "mock");
         // Start user -- this will update state of mUserController
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
+        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
         assertNotNull(reportMsg);
         UserState userState = (UserState) reportMsg.obj;
@@ -440,7 +436,6 @@
         // Call dispatchUserSwitch and verify that observer was called only once
         mInjector.mHandler.clearAllRecordedMessages();
         mUserController.dispatchUserSwitch(userState, oldUserId, newUserId);
-        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
         verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any());
         Set<Integer> expectedCodes = Collections.singleton(CONTINUE_USER_SWITCH_MSG);
         Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes();
@@ -463,6 +458,7 @@
         mUserController.registerUserSwitchObserver(observer, "mock");
         // Start user -- this will update state of mUserController
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
+        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
         assertNotNull(reportMsg);
         UserState userState = (UserState) reportMsg.obj;
@@ -471,7 +467,6 @@
         // Call dispatchUserSwitch and verify that observer was called only once
         mInjector.mHandler.clearAllRecordedMessages();
         mUserController.dispatchUserSwitch(userState, oldUserId, newUserId);
-        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
         verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any());
         // Verify that CONTINUE_USER_SWITCH_MSG is not sent (triggers timeout)
         Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes();
@@ -554,6 +549,7 @@
         expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG);
         if (backgroundUserStopping) {
             expectedCodes.add(CLEAR_USER_JOURNEY_SESSION_MSG);
+            expectedCodes.add(0); // this is for directly posting in stopping.
         }
         if (expectScheduleBackgroundUserStopping) {
             expectedCodes.add(SCHEDULED_STOP_BACKGROUND_USER_MSG);
@@ -1579,13 +1575,21 @@
         // mock the device to be secure in order to expect the keyguard to be shown
         when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
 
-        // call real showKeyguard method for this test
-        doCallRealMethod().when(mInjector).showKeyguard(any());
+        // call real lockDeviceNowAndWaitForKeyguardShown method for this test
+        doCallRealMethod().when(mInjector).lockDeviceNowAndWaitForKeyguardShown();
 
-        mUserController.completeUserSwitch(TEST_USER_ID1, TEST_USER_ID2);
+        // call startUser on a thread because we're expecting it to be blocked
+        Thread threadStartUser = new Thread(()-> {
+            mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
+        });
+        threadStartUser.start();
 
-        // make sure the switch is stalled by checking the UserSwitchingDialog is not dismissed yet
-        verify(mInjector, never()).dismissUserSwitchingDialog(any());
+        // make sure the switch is stalled...
+        Thread.sleep(2000);
+        // by checking REPORT_USER_SWITCH_MSG is not sent yet
+        assertNull(mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG));
+        // and the thread is still alive
+        assertTrue(threadStartUser.isAlive());
 
         // mock send the keyguard shown event
         ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass(
@@ -1593,42 +1597,12 @@
         verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture());
         captor.getValue().onKeyguardStateChanged(true);
 
-        // verify the switch now moves on by checking the UserSwitchingDialog is dismissed
-        verify(mInjector, atLeastOnce()).dismissUserSwitchingDialog(any());
-
-        // verify that SHOW_KEYGUARD_TIMEOUT is ignored and does not crash the system
-        try {
-            mInjector.mHandler.processPostDelayedCallbacksWithin(
-                    UserController.SHOW_KEYGUARD_TIMEOUT_MS);
-        } catch (RuntimeException e) {
-            throw new AssertionError(
-                    "SHOW_KEYGUARD_TIMEOUT is not ignored and crashed the system", e);
-        }
-    }
-
-    @Test
-    public void testRuntimeExceptionIsThrownIfTheKeyguardIsNotShown() throws Exception {
-        // enable user switch ui, because keyguard is only shown then
-        mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
-                /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false,
-                /* backgroundUserScheduledStopTimeSecs= */ -1);
-
-        // mock the device to be secure in order to expect the keyguard to be shown
-        when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
-
-        // suppress showKeyguard method for this test
-        doNothing().when(mInjector).showKeyguard(any());
-
-        mUserController.completeUserSwitch(TEST_USER_ID1, TEST_USER_ID2);
-
-        // verify that the system has crashed
-        assertThrows("Should have thrown RuntimeException", RuntimeException.class, () -> {
-            mInjector.mHandler.processPostDelayedCallbacksWithin(
-                    UserController.SHOW_KEYGUARD_TIMEOUT_MS);
-        });
-
-        // make sure the UserSwitchingDialog is not dismissed
-        verify(mInjector, never()).dismissUserSwitchingDialog(any());
+        // verify the switch now moves on...
+        Thread.sleep(1000);
+        // by checking REPORT_USER_SWITCH_MSG is sent
+        assertNotNull(mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG));
+        // and the thread is finished
+        assertFalse(threadStartUser.isAlive());
     }
 
     private void setUpAndStartUserInBackground(int userId) throws Exception {
@@ -1989,9 +1963,7 @@
         Set<Integer> getMessageCodes() {
             Set<Integer> result = new LinkedHashSet<>();
             for (Message msg : mMessages) {
-                if (msg.what != 0) { // ignore mHandle.post and mHandler.postDelayed messages
-                    result.add(msg.what);
-                }
+                result.add(msg.what);
             }
             return result;
         }
@@ -2015,28 +1987,14 @@
 
         @Override
         public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
-            final Runnable cb = msg.getCallback();
-            if (cb != null && uptimeMillis <= SystemClock.uptimeMillis()) {
-                // run mHandler.post calls immediately
-                cb.run();
-                return true;
-            }
             Message copy = new Message();
             copy.copyFrom(msg);
-            copy.setCallback(cb);
             mMessages.add(copy);
-            return super.sendMessageAtTime(msg, uptimeMillis);
-        }
-
-        public void processPostDelayedCallbacksWithin(long millis) {
-            final long whenMax = SystemClock.uptimeMillis() + millis;
-            for (Message msg : mMessages) {
-                final Runnable cb = msg.getCallback();
-                if (cb != null && msg.getWhen() <= whenMax) {
-                    msg.setCallback(null);
-                    cb.run();
-                }
+            if (msg.getCallback() != null) {
+                msg.getCallback().run();
+                msg.setCallback(null);
             }
+            return super.sendMessageAtTime(msg, uptimeMillis);
         }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 689b241..abc9ce3 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -50,11 +50,12 @@
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
+import android.annotation.SuppressLint;
 import android.app.ActivityManagerInternal;
 import android.app.ActivityOptions.LaunchCookie;
+import android.app.AppOpsManager;
 import android.app.KeyguardManager;
 import android.content.Context;
-import android.content.ContextWrapper;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.ApplicationInfoFlags;
@@ -72,6 +73,7 @@
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.testing.TestableContext;
 import android.view.ContentRecordingSession;
 import android.view.ContentRecordingSession.RecordContent;
 
@@ -99,13 +101,14 @@
 
 /**
  * Tests for the {@link MediaProjectionManagerService} class.
- *
+ * <p>
  * Build/Install/Run:
  * atest FrameworksServicesTests:MediaProjectionManagerServiceTest
  */
 @SmallTest
 @Presubmit
 @RunWith(AndroidJUnit4.class)
+@SuppressLint({"UseCheckPermission", "VisibleForTests", "MissingPermission"})
 public class MediaProjectionManagerServiceTest {
     private static final int UID = 10;
     private static final String PACKAGE_NAME = "test.package";
@@ -151,7 +154,10 @@
                 }
             };
 
-    private Context mContext;
+    @Rule
+    public final TestableContext mContext = spy(
+            new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()));
+
     private MediaProjectionManagerService mService;
     private OffsettableClock mClock;
     private ContentRecordingSession mWaitingDisplaySession =
@@ -169,6 +175,8 @@
     @Mock
     private KeyguardManager mKeyguardManager;
     @Mock
+    AppOpsManager mAppOpsManager;
+    @Mock
     private IMediaProjectionWatcherCallback mWatcherCallback;
     @Mock
     private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
@@ -185,10 +193,9 @@
         LocalServices.removeServiceForTest(WindowManagerInternal.class);
         LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
 
-        mContext = spy(new ContextWrapper(
-                InstrumentationRegistry.getInstrumentation().getTargetContext()));
-        doReturn(mPackageManager).when(mContext).getPackageManager();
-        doReturn(mKeyguardManager).when(mContext).getSystemService(eq(Context.KEYGUARD_SERVICE));
+        mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
+        mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
+        mContext.setMockPackageManager(mPackageManager);
 
         mClock = new OffsettableClock.Stopped();
         mWaitingDisplaySession.setWaitingForConsent(true);
@@ -291,6 +298,27 @@
         assertThat(mService.getActiveProjectionInfo()).isNotNull();
     }
 
+    @SuppressLint("MissingPermission")
+    @EnableFlags(android.companion.virtualdevice.flags
+            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+    @Test
+    public void testCreateProjection_keyguardLocked_AppOpMediaProjection()
+            throws NameNotFoundException {
+        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+        doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA),
+                eq(projection.uid), eq(projection.packageName));
+        doReturn(true).when(mKeyguardManager).isKeyguardLocked();
+
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+                RECORD_SENSITIVE_CONTENT, projection.packageName);
+
+        projection.start(mIMediaProjectionCallback);
+        projection.notifyVirtualDisplayCreated(10);
+
+        // The projection was started because it was allowed to capture the keyguard.
+        assertThat(mService.getActiveProjectionInfo()).isNotNull();
+    }
+
     @Test
     public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
             throws NameNotFoundException {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 96ddf80..b8f9767 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2815,7 +2815,8 @@
     }
 
     @Test
-    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
     public void testOnlyForceGroupIfNeeded_newNotification_notAutogrouped() {
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false);
         when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(false);
@@ -2834,7 +2835,8 @@
     }
 
     @Test
-    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
     public void testOnlyForceGroupIfNeeded_newNotification_wasAutogrouped() {
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false);
         when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 957b5e0..ae0c6e5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -331,6 +331,7 @@
         final WindowProcessController proc = mSystemServicesTestRule.addProcess(
                 activity.packageName, activity.processName,
                 6789 /* pid */, activity.info.applicationInfo.uid);
+        assertFalse(proc.mHasEverAttached);
         try {
             mRootWindowContainer.attachApplication(proc);
             verify(mSupervisor).realStartActivityLocked(eq(topActivity), eq(proc),
@@ -338,6 +339,15 @@
         } catch (RemoteException e) {
             e.rethrowAsRuntimeException();
         }
+
+        // Verify that onProcessRemoved won't clear the launching activities if an attached process
+        // is died. Because in real case, it should be handled from WindowProcessController's
+        // and ActivityRecord's handleAppDied to decide whether to remove the activities.
+        assertTrue(proc.mHasEverAttached);
+        assertTrue(mAtm.mStartingProcessActivities.isEmpty());
+        mAtm.mStartingProcessActivities.add(activity);
+        mAtm.mInternal.onProcessRemoved(proc.mName, proc.mUid);
+        assertFalse(mAtm.mStartingProcessActivities.isEmpty());
     }
 
     /**
diff --git a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
index 66a20ae..50e3a0e 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
@@ -34,4 +34,12 @@
      * @param isEmergency True means satellite enabled for emergency mode, false otherwise.
      */
     void onEmergencyModeChanged(in boolean isEmergency);
+
+    /**
+     * Indicates that the satellite registration failed with following failure code
+     *
+     * @param causeCode the primary failure cause code of the procedure.
+     *                  For LTE (EMM), cause codes are TS 24.301 Sec 9.9.3.9
+     */
+    void onRegistrationFailure(in int causeCode);
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 90dae3b..4eefaac 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -19,6 +19,7 @@
 import android.Manifest;
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
+import android.annotation.Hide;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1579,6 +1580,13 @@
                         executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                 callback.onEmergencyModeChanged(isEmergency)));
                     }
+
+                    @Hide
+                    @Override
+                    public void onRegistrationFailure(int causeCode) {
+                        executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                callback.onRegistrationFailure(causeCode)));
+                    }
                 };
                 sSatelliteModemStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForSatelliteModemStateChanged(internalCallback);
diff --git a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
index 423a785..13af469 100644
--- a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
@@ -45,4 +45,13 @@
      */
     @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     default void onEmergencyModeChanged(boolean isEmergency) {};
+
+    /**
+     * Indicates that the satellite registration failed with following failure code
+     *
+     * @param causeCode the primary failure cause code of the procedure.
+     *                  For LTE (EMM), cause codes are TS 24.301 Sec 9.9.3.9
+     * @hide
+     */
+    default void onRegistrationFailure(int causeCode) {};
 }
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 2a82d5f..351ec463 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -212,9 +212,10 @@
         verify(native).setMotionClassifierEnabled(anyBoolean())
         verify(native).setMaximumObscuringOpacityForTouch(anyFloat())
         verify(native).setStylusPointerIconEnabled(anyBoolean())
-        // Called twice at boot, since there are individual callbacks to update the
-        // key repeat timeout and the key repeat delay.
-        verify(native, times(2)).setKeyRepeatConfiguration(anyInt(), anyInt())
+        // Called thrice at boot, since there are individual callbacks to update the
+        // key repeat timeout, the key repeat delay and whether key repeat enabled.
+        verify(native, times(3)).setKeyRepeatConfiguration(anyInt(), anyInt(),
+            anyBoolean())
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java
similarity index 100%
rename from core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
rename to tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java
diff --git a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java b/tests/Tracing/src/android/tracing/perfetto/TestDataSource.java
similarity index 100%
rename from core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
rename to tests/Tracing/src/android/tracing/perfetto/TestDataSource.java
diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index e841d9e..cfb2645 100644
--- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.protolog;
 
+import static android.tools.traces.Utils.executeShellCommand;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -42,12 +44,13 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.internal.protolog.ProtoLogConfigurationService.ViewerConfigFileTracer;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer;
 import com.android.internal.protolog.common.IProtoLogGroup;
 import com.android.internal.protolog.common.LogDataType;
 import com.android.internal.protolog.common.LogLevel;
 
 import com.google.common.truth.Truth;
+import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.After;
 import org.junit.Before;
@@ -57,12 +60,14 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 
+import perfetto.protos.PerfettoConfig.TracingServiceState;
 import perfetto.protos.Protolog;
 import perfetto.protos.ProtologCommon;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -166,7 +171,8 @@
                 return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray());
             });
         };
-        sProtoLogConfigurationService = new ProtoLogConfigurationService(dataSourceBuilder, tracer);
+        sProtoLogConfigurationService =
+                new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer);
 
         if (android.tracing.Flags.clientSideProtoLogging()) {
             sProtoLog = new PerfettoProtoLogImpl(
@@ -177,6 +183,8 @@
                     viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(),
                     TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
         }
+
+        waitDataSourceIsAvailable();
     }
 
     @Before
@@ -862,6 +870,54 @@
                 .isEqualTo("This message should also be logged 567");
     }
 
+    private static void waitDataSourceIsAvailable() {
+        final int timeoutMs = 10000;
+        final int busyWaitIntervalMs = 100;
+
+        int elapsedMs = 0;
+
+        while (!isDataSourceAvailable()) {
+            SystemClock.sleep(busyWaitIntervalMs);
+            elapsedMs += busyWaitIntervalMs;
+            if (elapsedMs >= timeoutMs) {
+                throw new RuntimeException("Data source didn't become available."
+                        + " Waited for: " + timeoutMs + " ms");
+            }
+        }
+    }
+
+    private static boolean isDataSourceAvailable() {
+        byte[] proto = executeShellCommand("perfetto --query-raw");
+
+        try {
+            TracingServiceState state = TracingServiceState.parseFrom(proto);
+
+            Optional<Integer> producerId = Optional.empty();
+
+            for (TracingServiceState.Producer producer : state.getProducersList()) {
+                if (producer.getPid() == android.os.Process.myPid()) {
+                    producerId = Optional.of(producer.getId());
+                    break;
+                }
+            }
+
+            if (producerId.isEmpty()) {
+                return false;
+            }
+
+            for (TracingServiceState.DataSource ds : state.getDataSourcesList()) {
+                if (ds.getDsDescriptor().getName().equals(TEST_PROTOLOG_DATASOURCE_NAME)
+                        && ds.getProducerId() == producerId.get()) {
+                    return true;
+                }
+            }
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+
+        return false;
+    }
+
     private enum TestProtoLogGroup implements IProtoLogGroup {
         TEST_GROUP(true, true, false, "TEST_TAG");
 
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java
index e1bdd77..a3d03a8 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java
@@ -150,11 +150,11 @@
 
     @Test
     public void canRegisterClientWithGroupsOnly() throws RemoteException {
-        final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+        final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
 
-        final ProtoLogConfigurationService.RegisterClientArgs args =
-                new ProtoLogConfigurationService.RegisterClientArgs()
-                        .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+                new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                        .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(TEST_GROUP, true));
         service.registerClient(mMockClient, args);
 
@@ -165,11 +165,11 @@
     @Test
     public void willDumpViewerConfigOnlyOnceOnTraceStop()
             throws RemoteException, InvalidProtocolBufferException {
-        final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+        final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
 
-        final ProtoLogConfigurationService.RegisterClientArgs args =
-                new ProtoLogConfigurationService.RegisterClientArgs()
-                        .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+                new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                        .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(TEST_GROUP, true))
                         .setViewerConfigFile(mViewerConfigFile.getAbsolutePath());
         service.registerClient(mMockClient, args);
@@ -200,13 +200,13 @@
     @Test
     public void willDumpViewerConfigOnLastClientDisconnected()
             throws RemoteException, FileNotFoundException {
-        final ProtoLogConfigurationService.ViewerConfigFileTracer tracer =
-                Mockito.mock(ProtoLogConfigurationService.ViewerConfigFileTracer.class);
-        final ProtoLogConfigurationService service = new ProtoLogConfigurationService(tracer);
+        final ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer tracer =
+                Mockito.mock(ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer.class);
+        final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(tracer);
 
-        final ProtoLogConfigurationService.RegisterClientArgs args =
-                new ProtoLogConfigurationService.RegisterClientArgs()
-                        .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+                new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                        .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(TEST_GROUP, true))
                         .setViewerConfigFile(mViewerConfigFile.getAbsolutePath());
         service.registerClient(mMockClient, args);
@@ -225,10 +225,10 @@
 
     @Test
     public void sendEnableLoggingToLogcatToClient() throws RemoteException {
-        final var service = new ProtoLogConfigurationService();
+        final var service = new ProtoLogConfigurationServiceImpl();
 
-        final var args = new ProtoLogConfigurationService.RegisterClientArgs()
-                .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                         .GroupConfig(TEST_GROUP, false));
         service.registerClient(mMockClient, args);
 
@@ -242,11 +242,11 @@
 
     @Test
     public void sendDisableLoggingToLogcatToClient() throws RemoteException {
-        final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+        final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
 
-        final ProtoLogConfigurationService.RegisterClientArgs args =
-                new ProtoLogConfigurationService.RegisterClientArgs()
-                        .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+                new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                        .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(TEST_GROUP, true));
         service.registerClient(mMockClient, args);
 
@@ -260,11 +260,11 @@
 
     @Test
     public void doNotSendLoggingToLogcatToClientWithoutRegisteredGroup() throws RemoteException {
-        final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+        final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
 
-        final ProtoLogConfigurationService.RegisterClientArgs args =
-                new ProtoLogConfigurationService.RegisterClientArgs()
-                        .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+                new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                        .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(TEST_GROUP, false));
         service.registerClient(mMockClient, args);
 
@@ -277,15 +277,15 @@
 
     @Test
     public void handlesToggleToLogcatBeforeClientIsRegistered() throws RemoteException {
-        final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+        final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
 
         Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP);
         service.enableProtoLogToLogcat(TEST_GROUP);
         Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue();
 
-        final ProtoLogConfigurationService.RegisterClientArgs args =
-                new ProtoLogConfigurationService.RegisterClientArgs()
-                        .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+        final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+                new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+                        .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
                                 .GroupConfig(TEST_GROUP, false));
         service.registerClient(mMockClient, args);
 
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
index f68ae2c..fc4a909 100644
--- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
@@ -173,6 +173,10 @@
                 }
             }
         }
+
+        Executor getExecutor() {
+            return mExecutor;
+        }
     }
 
     private ISharedConnectivityService mService;
@@ -188,7 +192,7 @@
     private final String mServicePackageName;
     private final String mIntentAction;
     private ServiceConnection mServiceConnection;
-    private UserManager mUserManager;
+    private final UserManager mUserManager;
 
     /**
      * Creates a new instance of {@link SharedConnectivityManager}.
@@ -316,15 +320,19 @@
 
     private void registerCallbackInternal(SharedConnectivityClientCallback callback,
             SharedConnectivityCallbackProxy proxy) {
-        try {
-            mService.registerCallback(proxy);
-            synchronized (mProxyDataLock) {
-                mProxyMap.put(callback, proxy);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Exception in registerCallback", e);
-            callback.onRegisterCallbackFailed(e);
-        }
+        proxy.getExecutor().execute(
+                () -> {
+                    try {
+                        mService.registerCallback(proxy);
+                        synchronized (mProxyDataLock) {
+                            mProxyMap.put(callback, proxy);
+                        }
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Exception in registerCallback", e);
+                        callback.onRegisterCallbackFailed(e);
+                    }
+                }
+        );
     }
 
     /**