Merge "Fix missing bluetooth profiles" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 5c20aeb..3a772e1 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -60,6 +60,8 @@
     ":device_policy_aconfig_flags_lib{.generated_srcjars}",
     ":service-jobscheduler-deviceidle.flags-aconfig-java{.generated_srcjars}",
     ":surfaceflinger_flags_java_lib{.generated_srcjars}",
+    ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}",
+    ":android.hardware.usb.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -667,3 +669,29 @@
     aconfig_declarations: "surfaceflinger_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Content Capture
+aconfig_declarations {
+    name: "android.view.contentcapture.flags-aconfig",
+    package: "android.view.contentcapture.flags",
+    srcs: ["core/java/android/view/contentcapture/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.view.contentcapture.flags-aconfig-java",
+    aconfig_declarations: "android.view.contentcapture.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// USB
+aconfig_declarations {
+    name: "android.hardware.usb.flags-aconfig",
+    package: "android.hardware.usb.flags",
+    srcs: ["core/java/android/hardware/usb/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.hardware.usb.flags-aconfig-java",
+    aconfig_declarations: "android.hardware.usb.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 7d38377..12f455a 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -2539,6 +2539,38 @@
         pw.println("]");
         pw.println();
 
+        pw.println("mActiveAdminApps=[");
+        synchronized (mActiveAdminApps) {
+            final int size = mActiveAdminApps.size();
+            for (int i = 0; i < size; ++i) {
+                final int userId = mActiveAdminApps.keyAt(i);
+                pw.print(" ");
+                pw.print(userId);
+                pw.print(": ");
+                pw.print(mActiveAdminApps.valueAt(i));
+                if (i != size - 1) pw.print(",");
+                pw.println();
+            }
+        }
+        pw.println("]");
+        pw.println();
+
+        pw.println("mAdminProtectedPackages=[");
+        synchronized (mAdminProtectedPackages) {
+            final int size = mAdminProtectedPackages.size();
+            for (int i = 0; i < size; ++i) {
+                final int userId = mAdminProtectedPackages.keyAt(i);
+                pw.print(" ");
+                pw.print(userId);
+                pw.print(": ");
+                pw.print(mAdminProtectedPackages.valueAt(i));
+                if (i != size - 1) pw.print(",");
+                pw.println();
+            }
+        }
+        pw.println("]");
+        pw.println();
+
         mInjector.dump(pw);
     }
 
diff --git a/api/Android.bp b/api/Android.bp
index de4435e..4d56b37 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -334,6 +334,16 @@
     visibility: ["//frameworks/base/api"],
 }
 
+// We resolve dependencies on APIs in modules by depending on a prebuilt of the whole
+// platform (sdk_system_current_android). That prebuilt does not include module-lib APIs,
+// so use the prebuilt module-lib stubs for modules that export module-lib stubs that the
+// non-updatable part depends on.
+non_updatable_api_deps_on_modules = [
+    "sdk_module-lib_current_framework-tethering",
+    "sdk_module-lib_current_framework-connectivity-t",
+    "sdk_system_current_android",
+]
+
 // Defaults with module APIs in the classpath (mostly from prebuilts).
 // Suitable for compiling android-non-updatable.
 stubs_defaults {
@@ -345,19 +355,7 @@
             "packages/modules/Media/apex/aidl/stable",
         ],
     },
-    libs: [
-        "art.module.public.api",
-        "sdk_module-lib_current_framework-tethering",
-        "sdk_module-lib_current_framework-connectivity-t",
-        "sdk_public_current_framework-bluetooth",
-        // There are a few classes from modules used by the core that
-        // need to be resolved by metalava. We use a prebuilt stub of the
-        // full sdk to ensure we can resolve them. If a new class gets added,
-        // the prebuilts/sdk/current needs to be updated.
-        "sdk_system_current_android",
-        // NOTE: The below can be removed once the prebuilt stub contains IKE.
-        "sdk_system_current_android.net.ipsec.ike",
-    ],
+    libs: non_updatable_api_deps_on_modules,
 }
 
 // Defaults for the java_sdk_libraries of unbundled jars from framework.
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 5688b96..f6f6929 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -351,17 +351,7 @@
         "android-non-updatable_from_source_defaults",
     ],
     srcs: [":module-lib-api-stubs-docs-non-updatable"],
-    libs: [
-        // We cannot depend on all-modules-module-lib-stubs, because the module-lib stubs
-        // depend on this stub. We resolve dependencies on APIs in modules by depending
-        // on a prebuilt of the whole platform (sdk_system_current_android).
-        // That prebuilt does not include module-lib APIs, so use the prebuilt module-lib
-        // stubs for modules that export module-lib stubs that the non-updatable part
-        // depends on.
-        "sdk_module-lib_current_framework-tethering",
-        "sdk_module-lib_current_framework-connectivity-t",
-        "sdk_system_current_android",
-    ],
+    libs: non_updatable_api_deps_on_modules,
     dist: {
         dir: "apistubs/android/module-lib",
     },
diff --git a/cmds/gpu_counter_producer/main.cpp b/cmds/gpu_counter_producer/main.cpp
index 1054cba..4616638 100644
--- a/cmds/gpu_counter_producer/main.cpp
+++ b/cmds/gpu_counter_producer/main.cpp
@@ -133,6 +133,12 @@
         daemon(0, 0);
     }
 
+    if (getenv("LD_LIBRARY_PATH") == nullptr) {
+        setenv("LD_LIBRARY_PATH", "/vendor/lib64:/vendor/lib", 0 /*override*/);
+        LOG_INFO("execv with: LD_LIBRARY_PATH=%s", getenv("LD_LIBRARY_PATH"));
+        execvpe(pname, argv, environ);
+    }
+
     if (!writeToPidFile()) {
         LOG_ERR("Could not open %s", kPidFileName);
         return 1;
diff --git a/core/api/current.txt b/core/api/current.txt
index e8a6ac9..87f5b3c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -18634,7 +18634,7 @@
     method @Deprecated @Nullable public android.security.identity.IdentityCredential getIdentityCredential();
     method @FlaggedApi("android.hardware.biometrics.add_key_agreement_crypto_object") @Nullable public javax.crypto.KeyAgreement getKeyAgreement();
     method @Nullable public javax.crypto.Mac getMac();
-    method @FlaggedApi("android.hardware.biometrics.get_op_id_crypto_object") public long getOpId();
+    method @FlaggedApi("android.hardware.biometrics.get_op_id_crypto_object") public long getOperationHandle();
     method @Nullable public android.security.identity.PresentationSession getPresentationSession();
     method @Nullable public java.security.Signature getSignature();
   }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 441dcae..739fdc5 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -133,6 +133,7 @@
     field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
     field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA";
     field public static final String GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS";
+    field @FlaggedApi("android.app.get_binding_uid_importance") public static final String GET_BINDING_UID_IMPORTANCE = "android.permission.GET_BINDING_UID_IMPORTANCE";
     field public static final String GET_HISTORICAL_APP_OPS_STATS = "android.permission.GET_HISTORICAL_APP_OPS_STATS";
     field public static final String GET_PROCESS_STATE_AND_OOM_SCORE = "android.permission.GET_PROCESS_STATE_AND_OOM_SCORE";
     field public static final String GET_RUNTIME_PERMISSIONS = "android.permission.GET_RUNTIME_PERMISSIONS";
@@ -367,6 +368,7 @@
     field public static final String SYSTEM_APPLICATION_OVERLAY = "android.permission.SYSTEM_APPLICATION_OVERLAY";
     field public static final String SYSTEM_CAMERA = "android.permission.SYSTEM_CAMERA";
     field public static final String TETHER_PRIVILEGED = "android.permission.TETHER_PRIVILEGED";
+    field @FlaggedApi("com.android.net.thread.flags.thread_enabled") public static final String THREAD_NETWORK_PRIVILEGED = "android.permission.THREAD_NETWORK_PRIVILEGED";
     field public static final String TIS_EXTENSION_INTERFACE = "android.permission.TIS_EXTENSION_INTERFACE";
     field public static final String TOGGLE_AUTOMOTIVE_PROJECTION = "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION";
     field public static final String TRIGGER_LOST_MODE = "android.permission.TRIGGER_LOST_MODE";
@@ -543,6 +545,7 @@
   public class ActivityManager {
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int);
     method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String);
+    method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int);
     method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser();
     method @FlaggedApi("android.app.app_start_info") @NonNull @RequiresPermission(android.Manifest.permission.DUMP) public java.util.List<android.app.ApplicationStartInfo> getExternalHistoricalProcessStartReasons(@NonNull String, @IntRange(from=0) int);
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int getPackageImportance(String);
@@ -6186,8 +6189,13 @@
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int COMPLIANCE_WARNING_BC_1_2 = 3; // 0x3
     field public static final int COMPLIANCE_WARNING_DEBUG_ACCESSORY = 2; // 0x2
+    field @FlaggedApi("android.hardware.usb.flags.enable_usb_data_compliance_warning") public static final int COMPLIANCE_WARNING_ENUMERATION_FAIL = 7; // 0x7
+    field @FlaggedApi("android.hardware.usb.flags.enable_usb_data_compliance_warning") public static final int COMPLIANCE_WARNING_FLAKY_CONNECTION = 8; // 0x8
+    field @FlaggedApi("android.hardware.usb.flags.enable_usb_data_compliance_warning") public static final int COMPLIANCE_WARNING_INPUT_POWER_LIMITED = 5; // 0x5
+    field @FlaggedApi("android.hardware.usb.flags.enable_usb_data_compliance_warning") public static final int COMPLIANCE_WARNING_MISSING_DATA_LINES = 6; // 0x6
     field public static final int COMPLIANCE_WARNING_MISSING_RP = 4; // 0x4
     field public static final int COMPLIANCE_WARNING_OTHER = 1; // 0x1
+    field @FlaggedApi("android.hardware.usb.flags.enable_usb_data_compliance_warning") public static final int COMPLIANCE_WARNING_UNRELIABLE_IO = 9; // 0x9
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.usb.UsbPortStatus> CREATOR;
     field public static final int DATA_ROLE_DEVICE = 2; // 0x2
     field public static final int DATA_ROLE_HOST = 1; // 0x1
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index b2bfda1..8b20720 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3592,8 +3592,8 @@
     field public static final int ACCESSIBILITY_TITLE_CHANGED = 33554432; // 0x2000000
     field public static final int FLAG_SLIPPERY = 536870912; // 0x20000000
     field public CharSequence accessibilityTitle;
-    field @FlaggedApi("android.view.flags.wm_display_refresh_rate_test") public float preferredMaxDisplayRefreshRate;
-    field @FlaggedApi("android.view.flags.wm_display_refresh_rate_test") public float preferredMinDisplayRefreshRate;
+    field public float preferredMaxDisplayRefreshRate;
+    field public float preferredMinDisplayRefreshRate;
     field public int privateFlags;
   }
 
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index f68681b..8b4ebae 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -73,7 +73,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -4269,6 +4268,33 @@
     }
 
     /**
+     * Same as {@link #getUidImportance(int)}, but it only works on UIDs that currently
+     * have a service binding, or provider reference, to the calling UID, even if the target UID
+     * belong to another android user or profile.
+     *
+     * <p>This will return {@link RunningAppProcessInfo#IMPORTANCE_GONE} on all other UIDs,
+     * regardless of if they're valid or not.
+     *
+     * <p>Privileged system apps may prefer this API to {@link #getUidImportance(int)} to
+     * avoid requesting the permission {@link Manifest.permission#PACKAGE_USAGE_STATS}, which
+     * would allow access to APIs that return more senstive information.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_GET_BINDING_UID_IMPORTANCE)
+    @SystemApi
+    @RequiresPermission(Manifest.permission.GET_BINDING_UID_IMPORTANCE)
+    public @RunningAppProcessInfo.Importance int getBindingUidImportance(int uid) {
+        try {
+            int procState = getService().getBindingUidProcessState(uid,
+                    mContext.getOpPackageName());
+            return RunningAppProcessInfo.procStateToImportanceForClient(procState, mContext);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Callback to get reports about changes to the importance of a uid.  Use with
      * {@link #addOnUidImportanceListener}.
      * @hide
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index dfb416a..c136db6 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -56,7 +56,7 @@
 import android.app.backup.BackupAnnotations.OperationType;
 import android.app.compat.CompatChanges;
 import android.app.sdksandbox.sandboxactivity.ActivityContextInfo;
-import android.app.sdksandbox.sandboxactivity.SdkSandboxActivityAuthority;
+import android.app.sdksandbox.sandboxactivity.ActivityContextInfoProvider;
 import android.app.servertransaction.ActivityLifecycleItem;
 import android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
 import android.app.servertransaction.ActivityRelaunchItem;
@@ -378,6 +378,15 @@
     /** Maps from activity token to the pending override configuration. */
     @GuardedBy("mPendingOverrideConfigs")
     private final ArrayMap<IBinder, Configuration> mPendingOverrideConfigs = new ArrayMap<>();
+
+    /**
+     * A queue of pending ApplicationInfo updates. In case when we get a concurrent update
+     * this queue allows us to only apply the latest object, and it can be applied on demand
+     * instead of waiting for the handler thread to reach the scheduled callback.
+     */
+    @GuardedBy("mResourcesManager")
+    private final ArrayMap<String, ApplicationInfo> mPendingAppInfoUpdates = new ArrayMap<>();
+
     /** The activities to be truly destroyed (not include relaunch). */
     final Map<IBinder, DestroyActivityItem> mActivitiesToBeDestroyed =
             Collections.synchronizedMap(new ArrayMap<>());
@@ -1326,9 +1335,19 @@
         }
 
         public void scheduleApplicationInfoChanged(ApplicationInfo ai) {
+            synchronized (mResourcesManager) {
+                var oldAi = mPendingAppInfoUpdates.put(ai.packageName, ai);
+                if (oldAi != null && oldAi.createTimestamp > ai.createTimestamp) {
+                    Slog.w(TAG, "Skipping application info changed for obsolete AI with TS "
+                            + ai.createTimestamp + " < already pending TS "
+                            + oldAi.createTimestamp);
+                    mPendingAppInfoUpdates.put(ai.packageName, oldAi);
+                    return;
+                }
+            }
             mResourcesManager.appendPendingAppInfoUpdate(new String[]{ai.sourceDir}, ai);
-            mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai);
-            sendMessage(H.APPLICATION_INFO_CHANGED, ai);
+            mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai.packageName);
+            sendMessage(H.APPLICATION_INFO_CHANGED, ai.packageName);
         }
 
         public void updateTimeZone() {
@@ -2272,8 +2291,7 @@
                     case DUMP_HEAP: return "DUMP_HEAP";
                     case DUMP_ACTIVITY: return "DUMP_ACTIVITY";
                     case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS";
-                    case UPDATE_PACKAGE_COMPATIBILITY_INFO:
-                        return "UPDATE_PACKAGE_COMPATIBILITY_INFO";
+                    case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO";
                     case DUMP_PROVIDER: return "DUMP_PROVIDER";
                     case UNSTABLE_PROVIDER_DIED: return "UNSTABLE_PROVIDER_DIED";
                     case REQUEST_ASSIST_CONTEXT_EXTRAS: return "REQUEST_ASSIST_CONTEXT_EXTRAS";
@@ -2508,7 +2526,7 @@
                     break;
                 }
                 case APPLICATION_INFO_CHANGED:
-                    handleApplicationInfoChanged((ApplicationInfo) msg.obj);
+                    applyPendingApplicationInfoChanges((String) msg.obj);
                     break;
                 case RUN_ISOLATED_ENTRY_POINT:
                     handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1,
@@ -3777,10 +3795,8 @@
                     r.activityInfo.targetActivity);
         }
 
-        boolean isSandboxActivityContext =
-                sandboxActivitySdkBasedContext()
-                        && SdkSandboxActivityAuthority.isSdkSandboxActivity(
-                                mSystemContext, r.intent);
+        boolean isSandboxActivityContext = sandboxActivitySdkBasedContext()
+                && r.intent.isSandboxActivity(mSystemContext);
         boolean isSandboxedSdkContextUsed = false;
         ContextImpl activityBaseContext;
         if (isSandboxActivityContext) {
@@ -4025,12 +4041,11 @@
      */
     @Nullable
     private ContextImpl createBaseContextForSandboxActivity(@NonNull ActivityClientRecord r) {
-        SdkSandboxActivityAuthority sdkSandboxActivityAuthority =
-                SdkSandboxActivityAuthority.getInstance();
+        ActivityContextInfoProvider contextInfoProvider = ActivityContextInfoProvider.getInstance();
 
         ActivityContextInfo contextInfo;
         try {
-            contextInfo = sdkSandboxActivityAuthority.getActivityContextInfo(r.intent);
+            contextInfo = contextInfoProvider.getActivityContextInfo(r.intent);
         } catch (IllegalArgumentException e) {
             Log.e(TAG, "Passed intent does not match an expected sandbox activity", e);
             return null;
@@ -4074,7 +4089,8 @@
             mProfiler.startProfiling();
         }
 
-        // Make sure we are running with the most recent config.
+        // Make sure we are running with the most recent config and resource paths.
+        applyPendingApplicationInfoChanges(r.activityInfo.packageName);
         mConfigurationController.handleConfigurationChanged(null, null);
         updateDeviceIdForNonUIContexts(deviceId);
 
@@ -6442,6 +6458,17 @@
         r.mLastReportedWindowingMode = newWindowingMode;
     }
 
+    private void applyPendingApplicationInfoChanges(String packageName) {
+        final ApplicationInfo ai;
+        synchronized (mResourcesManager) {
+            ai = mPendingAppInfoUpdates.remove(packageName);
+        }
+        if (ai == null) {
+            return;
+        }
+        handleApplicationInfoChanged(ai);
+    }
+
     /**
      * Updates the application info.
      *
@@ -6467,6 +6494,16 @@
             apk = ref != null ? ref.get() : null;
             ref = mResourcePackages.get(ai.packageName);
             resApk = ref != null ? ref.get() : null;
+            for (ActivityClientRecord ar : mActivities.values()) {
+                if (ar.activityInfo.applicationInfo.packageName.equals(ai.packageName)) {
+                    ar.activityInfo.applicationInfo = ai;
+                    if (apk != null || resApk != null) {
+                        ar.packageInfo = apk != null ? apk : resApk;
+                    } else {
+                        apk = ar.packageInfo;
+                    }
+                }
+            }
         }
 
         if (apk != null) {
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 520bf7d..260e985 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -949,4 +949,5 @@
      * @param err The binder transaction error
      */
     oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err);
+    int getBindingUidProcessState(int uid, in String callingPackage);
 }
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index ebf183f..f1e44cc 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -342,7 +342,9 @@
      */
     public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
             @Nullable List<String> oldPaths) {
-        setApplicationInfo(aInfo);
+        if (!setApplicationInfo(aInfo)) {
+            return;
+        }
 
         final List<String> newPaths = new ArrayList<>();
         makePaths(mActivityThread, aInfo, newPaths);
@@ -387,7 +389,13 @@
         mAppComponentFactory = createAppFactory(aInfo, mDefaultClassLoader);
     }
 
-    private void setApplicationInfo(ApplicationInfo aInfo) {
+    private boolean setApplicationInfo(ApplicationInfo aInfo) {
+        if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) {
+            Slog.w(TAG, "New application info for package " + aInfo.packageName
+                    + " is out of date with TS " + aInfo.createTimestamp + " < the current TS "
+                    + mApplicationInfo.createTimestamp);
+            return false;
+        }
         final int myUid = Process.myUid();
         aInfo = adjustNativeLibraryPaths(aInfo);
         mApplicationInfo = aInfo;
@@ -410,6 +418,7 @@
         if (aInfo.requestsIsolatedSplitLoading() && !ArrayUtils.isEmpty(mSplitNames)) {
             mSplitLoader = new SplitDependencyLoaderImpl(aInfo.splitDependencies);
         }
+        return true;
     }
 
     void setSdkSandboxStorage(@Nullable String sdkSandboxClientAppVolumeUuid,
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 2076e85..b303ea6 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -5,4 +5,11 @@
      name: "app_start_info"
      description: "Control collecting of ApplicationStartInfo records and APIs."
      bug: "247814855"
-}
\ No newline at end of file
+}
+
+flag {
+     namespace: "backstage_power"
+     name: "get_binding_uid_importance"
+     description: "API to get importance of UID that's binding to the caller"
+     bug: "292533010"
+}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 02e0cf6..ea54c91 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -12587,12 +12587,8 @@
         return (mFlags & FLAG_ACTIVITY_NEW_DOCUMENT) == FLAG_ACTIVITY_NEW_DOCUMENT;
     }
 
-    /**
-     * @deprecated Use {@link SdkSandboxActivityAuthority#isSdkSandboxActivity} instead.
-     * Once the other API is finalized this method will be removed.
-     * @hide
-     */
-    @Deprecated
+    // TODO(b/299109198): Refactor into the {@link SdkSandboxManagerLocal}
+    /** @hide */
     public boolean isSandboxActivity(@NonNull Context context) {
         if (mAction != null && mAction.equals(ACTION_START_SANDBOXED_ACTIVITY)) {
             return true;
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 43cf97f..9ec082a 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -35,3 +35,10 @@
     description: "Further framework support for communal profile, beyond the basics, for later releases."
     bug: "285426179"
 }
+
+flag {
+    name: "use_all_cpus_during_user_switch"
+    namespace: "multiuser"
+    description: "Allow using all cpu cores during a user switch."
+    bug: "308105403"
+}
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 9b819a7..ec96215 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -13,3 +13,10 @@
     description: "Enables Credential Manager to work with Instant Apps"
     bug: "302190269"
 }
+
+flag {
+    namespace: "credential_manager"
+    name: "clear_session_enabled"
+    description: "Enables clearing of Credential Manager sessions when client process dies"
+    bug: "308470501"
+}
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
index 7b874cc..2081ced 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -47,6 +47,8 @@
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Static utility methods for dealing with databases and {@link Cursor}s.
@@ -1585,6 +1587,38 @@
     }
 
     /**
+     * A regular expression that matches the first three characters in a SQL statement, after
+     * skipping past comments and whitespace.  PREFIX_GROUP_NUM is the regex group that contains
+     * the matching prefix string.  If PREFIX_REGEX is changed, PREFIX_GROUP_NUM may require an
+     * update too.
+     */
+    private static final String PREFIX_REGEX =
+            "("                                         // Zero-or more...
+            + "\\s+"                                    //   Leading space
+            + "|"
+            + "--.*?\n"                                 //   Line comment
+            + "|"
+            + "/\\*[\\w\\W]*?\\*/"                      //   Block comment
+            + ")*"
+            + "(\\w\\w\\w)";                            // Three word-characters
+    private static final int PREFIX_GROUP_NUM = 2;
+    private static final Pattern sPrefixPattern = Pattern.compile(PREFIX_REGEX);
+
+    /**
+     * Return the three-letter prefix of a SQL statement, skipping past whitespace and comments.
+     * Comments either start with "--" and run to the end of the line or are C-style block
+     * comments.  The function returns null if a prefix could not be found.
+     */
+    private static String getSqlStatementPrefixExtended(String sql) {
+        Matcher m = sPrefixPattern.matcher(sql);
+        if (m.lookingAt()) {
+            return m.group(PREFIX_GROUP_NUM).toUpperCase(Locale.ROOT);
+        } else {
+            return null;
+        }
+    }
+
+    /**
      * Return the extended statement type for the SQL statement.  This is not a public API and it
      * can return values that are not publicly visible.
      * @hide
@@ -1630,6 +1664,9 @@
      */
     public static int getSqlStatementTypeExtended(@NonNull String sql) {
         int type = categorizeStatement(getSqlStatementPrefixSimple(sql), sql);
+        if (type == STATEMENT_COMMENT) {
+            type = categorizeStatement(getSqlStatementPrefixExtended(sql), sql);
+        }
         return type;
     }
 
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 97bbfbb..8c1ea5f 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -857,7 +857,7 @@
          * Get the operation handle associated with this object or 0 if none.
          */
         @FlaggedApi(FLAG_GET_OP_ID_CRYPTO_OBJECT)
-        public long getOpId() {
+        public long getOperationHandle() {
             return super.getOpId();
         }
     }
diff --git a/core/java/android/hardware/usb/UsbPort.java b/core/java/android/hardware/usb/UsbPort.java
index 490b128..8f0149b 100644
--- a/core/java/android/hardware/usb/UsbPort.java
+++ b/core/java/android/hardware/usb/UsbPort.java
@@ -52,6 +52,11 @@
 import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_BC_1_2;
 import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP;
 import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_OTHER;
+import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_INPUT_POWER_LIMITED;
+import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_MISSING_DATA_LINES;
+import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_ENUMERATION_FAIL;
+import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_FLAKY_CONNECTION;
+import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_UNRELIABLE_IO;
 import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_UNKNOWN;
 import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_NOT_CAPABLE;
 import static android.hardware.usb.DisplayPortAltModeInfo.DISPLAYPORT_ALT_MODE_STATUS_CAPABLE_DISABLED;
@@ -789,6 +794,21 @@
                     case UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP:
                         complianceWarningString.append("missing rp, ");
                         break;
+                    case UsbPortStatus.COMPLIANCE_WARNING_INPUT_POWER_LIMITED:
+                        complianceWarningString.append("input power limited, ");
+                        break;
+                    case UsbPortStatus.COMPLIANCE_WARNING_MISSING_DATA_LINES:
+                        complianceWarningString.append("missing data lines, ");
+                        break;
+                    case UsbPortStatus.COMPLIANCE_WARNING_ENUMERATION_FAIL:
+                        complianceWarningString.append("enumeration fail, ");
+                        break;
+                    case UsbPortStatus.COMPLIANCE_WARNING_FLAKY_CONNECTION:
+                        complianceWarningString.append("flaky connection, ");
+                        break;
+                    case UsbPortStatus.COMPLIANCE_WARNING_UNRELIABLE_IO:
+                        complianceWarningString.append("unreliable io, ");
+                        break;
                     default:
                         complianceWarningString.append(String.format("Unknown(%d), ", warning));
                         break;
diff --git a/core/java/android/hardware/usb/UsbPortStatus.java b/core/java/android/hardware/usb/UsbPortStatus.java
index b4fe3a2..d959240 100644
--- a/core/java/android/hardware/usb/UsbPortStatus.java
+++ b/core/java/android/hardware/usb/UsbPortStatus.java
@@ -18,11 +18,13 @@
 
 import android.Manifest;
 import android.annotation.CheckResult;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.hardware.usb.flags.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -310,6 +312,54 @@
     public static final int COMPLIANCE_WARNING_MISSING_RP = 4;
 
     /**
+     * Used to indicate the charging setups on the USB ports are unable to
+     * deliver negotiated power. Introduced in Android V (API level 35)
+     * and client applicantions that target API levels lower than 35 will
+     * receive {@link #COMPLIANCE_WARNING_OTHER} instead.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_USB_DATA_COMPLIANCE_WARNING)
+    public static final int COMPLIANCE_WARNING_INPUT_POWER_LIMITED = 5;
+
+    /**
+     * Used to indicate the cable/connector on the USB ports are missing
+     * the required wires on the data pins to make data transfer.
+     * Introduced in Android V (API level 35) and client applicantions that
+     * target API levels lower than 35 will receive
+     * {@link #COMPLIANCE_WARNING_OTHER} instead.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_USB_DATA_COMPLIANCE_WARNING)
+    public static final int COMPLIANCE_WARNING_MISSING_DATA_LINES = 6;
+
+    /**
+     * Used to indicate enumeration failures on the USB ports, potentially due to
+     * signal integrity issues or other causes. Introduced in Android V
+     * (API level 35) and client applicantions that target API levels lower
+     * than 35 will receive {@link #COMPLIANCE_WARNING_OTHER} instead.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_USB_DATA_COMPLIANCE_WARNING)
+    public static final int COMPLIANCE_WARNING_ENUMERATION_FAIL = 7;
+
+    /**
+     * Used to indicate unexpected data disconnection on the USB ports,
+     * potentially due to signal integrity issues or other causes.
+     * Introduced in Android V (API level 35) and client applicantions that
+     * target API levels lower than 35 will receive
+     * {@link #COMPLIANCE_WARNING_OTHER} instead.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_USB_DATA_COMPLIANCE_WARNING)
+    public static final int COMPLIANCE_WARNING_FLAKY_CONNECTION = 8;
+
+    /**
+     * Used to indicate unreliable or slow data transfer on the USB ports,
+     * potentially due to signal integrity issues or other causes.
+     * Introduced in Android V (API level 35) and client applicantions that
+     * target API levels lower than 35 will receive
+     * {@link #COMPLIANCE_WARNING_OTHER} instead.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_USB_DATA_COMPLIANCE_WARNING)
+    public static final int COMPLIANCE_WARNING_UNRELIABLE_IO = 9;
+
+    /**
      * Indicates that the Type-C plug orientation cannot be
      * determined because the connected state of the device is unknown.
      */
@@ -372,6 +422,11 @@
             COMPLIANCE_WARNING_DEBUG_ACCESSORY,
             COMPLIANCE_WARNING_BC_1_2,
             COMPLIANCE_WARNING_MISSING_RP,
+            COMPLIANCE_WARNING_INPUT_POWER_LIMITED,
+            COMPLIANCE_WARNING_MISSING_DATA_LINES,
+            COMPLIANCE_WARNING_ENUMERATION_FAIL,
+            COMPLIANCE_WARNING_FLAKY_CONNECTION,
+            COMPLIANCE_WARNING_UNRELIABLE_IO,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ComplianceWarning{}
@@ -591,7 +646,12 @@
      * @return array including {@link #COMPLIANCE_WARNING_OTHER},
      *         {@link #COMPLIANCE_WARNING_DEBUG_ACCESSORY},
      *         {@link #COMPLIANCE_WARNING_BC_1_2},
-     *         or {@link #COMPLIANCE_WARNING_MISSING_RP}
+     *         {@link #COMPLIANCE_WARNING_MISSING_RP},
+     *         {@link #COMPLIANCE_WARNING_INPUT_POWER_LIMITED},
+     *         {@link #COMPLIANCE_WARNING_MISSING_DATA_LINES},
+     *         {@link #COMPLIANCE_WARNING_ENUMERATION_FAIL},
+     *         {@link #COMPLIANCE_WARNING_FLAKY_CONNECTION},
+     *         {@link #COMPLIANCE_WARNING_UNRELIABLE_IO}.
      */
     @CheckResult
     @NonNull
diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
new file mode 100644
index 0000000..6b78d05
--- /dev/null
+++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.hardware.usb.flags"
+
+flag {
+    name: "enable_usb_data_compliance_warning"
+    namespace: "system_sw_usb"
+    description: "Enable USB data compliance warnings when set"
+    bug: "296119135"
+}
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 47b6d8d..94f90cc 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -2018,9 +2018,13 @@
             return;
         }
 
+        // Temporarily disable checks so that explicit GC is allowed.
+        final int oldMask = getThreadPolicyMask();
+        setThreadPolicyMask(0);
         System.gc();
         System.runFinalization();
         System.gc();
+        setThreadPolicyMask(oldMask);
 
         // Note: classInstanceLimit is immutable, so this is lock-free
         // Create the classes array.
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index bcda25a..27ad45d 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -3206,15 +3206,6 @@
         public static final String INFRASTRUCTURE_BITMASK = "infrastructure_bitmask";
 
         /**
-         * Indicating if the APN is used for eSIM bootsrap provisioning. The default value is 0 (Not
-         * used for eSIM bootstrap provisioning).
-         *
-         * <P>Type: INTEGER</P>
-         * @hide
-         */
-        public static final String ESIM_BOOTSTRAP_PROVISIONING = "esim_bootstrap_provisioning";
-
-        /**
          * MVNO type:
          * {@code SPN (Service Provider Name), IMSI, GID (Group Identifier Level 1)}.
          * <P>Type: TEXT</P>
diff --git a/core/java/android/util/ArraySet.java b/core/java/android/util/ArraySet.java
index 73114e2..adebe2c 100644
--- a/core/java/android/util/ArraySet.java
+++ b/core/java/android/util/ArraySet.java
@@ -20,8 +20,6 @@
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 
-import libcore.util.EmptyArray;
-
 import java.lang.reflect.Array;
 import java.util.Arrays;
 import java.util.Collection;
diff --git a/core/java/android/util/Base64.java b/core/java/android/util/Base64.java
index 33cc5e3..92abd7c 100644
--- a/core/java/android/util/Base64.java
+++ b/core/java/android/util/Base64.java
@@ -26,7 +26,6 @@
  * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
  * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
  */
-@android.ravenwood.annotations.RavenwoodWholeClassKeep
 public class Base64 {
     /**
      * Default values for encoder/decoder flags.
diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java
index ac76fc2..c04a71c 100644
--- a/core/java/android/util/IntArray.java
+++ b/core/java/android/util/IntArray.java
@@ -19,8 +19,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
 
-import libcore.util.EmptyArray;
-
 import java.util.Arrays;
 
 /**
diff --git a/core/java/android/util/LongArray.java b/core/java/android/util/LongArray.java
index 9f269ed..3101c0d 100644
--- a/core/java/android/util/LongArray.java
+++ b/core/java/android/util/LongArray.java
@@ -23,8 +23,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
 
-import libcore.util.EmptyArray;
-
 import java.util.Arrays;
 
 /**
diff --git a/core/java/android/util/LongArrayQueue.java b/core/java/android/util/LongArrayQueue.java
index 5c701db..354f8df 100644
--- a/core/java/android/util/LongArrayQueue.java
+++ b/core/java/android/util/LongArrayQueue.java
@@ -19,8 +19,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
-import libcore.util.EmptyArray;
-
 import java.util.NoSuchElementException;
 
 /**
diff --git a/core/java/android/util/LongSparseArray.java b/core/java/android/util/LongSparseArray.java
index 2f14b25..8402ab2 100644
--- a/core/java/android/util/LongSparseArray.java
+++ b/core/java/android/util/LongSparseArray.java
@@ -24,8 +24,6 @@
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.function.LongObjPredicate;
 
-import libcore.util.EmptyArray;
-
 import java.util.Arrays;
 import java.util.Objects;
 
diff --git a/core/java/android/util/LongSparseLongArray.java b/core/java/android/util/LongSparseLongArray.java
index f23ec91..d4a0126 100644
--- a/core/java/android/util/LongSparseLongArray.java
+++ b/core/java/android/util/LongSparseLongArray.java
@@ -23,8 +23,6 @@
 import com.android.internal.util.GrowingArrayUtils;
 import com.android.internal.util.Preconditions;
 
-import libcore.util.EmptyArray;
-
 /**
  * Map of {@code long} to {@code long}. Unlike a normal array of longs, there
  * can be gaps in the indices. It is intended to be more memory efficient than using a
diff --git a/core/java/android/util/Range.java b/core/java/android/util/Range.java
index 41c171a..750696b 100644
--- a/core/java/android/util/Range.java
+++ b/core/java/android/util/Range.java
@@ -19,7 +19,8 @@
 import static com.android.internal.util.Preconditions.*;
 
 import android.annotation.Nullable;
-import android.hardware.camera2.utils.HashCodeHelpers;
+
+import java.util.Objects;
 
 /**
  * Immutable class for describing the range of two numeric values.
@@ -351,7 +352,7 @@
      */
     @Override
     public int hashCode() {
-        return HashCodeHelpers.hashCodeGeneric(mLower, mUpper);
+        return Objects.hash(mLower, mUpper);
     }
 
     private final T mLower;
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index cd03d83..c18cac3 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -22,8 +22,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
-import libcore.util.EmptyArray;
-
 import java.util.Objects;
 
 /**
diff --git a/core/java/android/util/SparseBooleanArray.java b/core/java/android/util/SparseBooleanArray.java
index 12a9900..795f4c9 100644
--- a/core/java/android/util/SparseBooleanArray.java
+++ b/core/java/android/util/SparseBooleanArray.java
@@ -22,8 +22,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
-import libcore.util.EmptyArray;
-
 /**
  * SparseBooleanArrays map integers to booleans.
  * Unlike a normal array of booleans
diff --git a/core/java/android/util/SparseIntArray.java b/core/java/android/util/SparseIntArray.java
index 0e98c28..24d04be 100644
--- a/core/java/android/util/SparseIntArray.java
+++ b/core/java/android/util/SparseIntArray.java
@@ -21,8 +21,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
-import libcore.util.EmptyArray;
-
 import java.util.Arrays;
 
 /**
diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java
index e86b647..4b257e6 100644
--- a/core/java/android/util/SparseLongArray.java
+++ b/core/java/android/util/SparseLongArray.java
@@ -19,8 +19,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
-import libcore.util.EmptyArray;
-
 /**
  * SparseLongArrays map integers to longs.  Unlike a normal array of longs,
  * there can be gaps in the indices.  It is intended to be more memory efficient
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index 42ac74c..ad326e4 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -11,6 +11,7 @@
 roosa@google.com
 jreck@google.com
 siyamed@google.com
+mount@google.com
 
 # Autofill
 per-file ViewStructure.java = file:/core/java/android/service/autofill/OWNERS
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c9c1f20..d97dfb0 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -123,6 +123,7 @@
 import android.graphics.BLASTBufferQueue;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ForceDarkType;
 import android.graphics.FrameInfo;
 import android.graphics.HardwareRenderer;
 import android.graphics.HardwareRenderer.FrameDrawingCallback;
@@ -1796,7 +1797,7 @@
 
     /** Returns true if force dark should be enabled according to various settings */
     @VisibleForTesting
-    public boolean isForceDarkEnabled() {
+    public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
         if (forceInvertColor()) {
             boolean isForceInvertEnabled = Settings.Secure.getIntForUser(
                     mContext.getContentResolver(),
@@ -1808,7 +1809,7 @@
             // for dark mode in configuration.uiMode. Instead, we assume that the force invert
             // setting will be enabled at the same time dark theme is in the Settings app.
             if (isForceInvertEnabled) {
-                return true;
+                return ForceDarkType.FORCE_INVERT_COLOR_DARK;
             }
         }
 
@@ -1822,12 +1823,12 @@
                     && a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
             a.recycle();
         }
-        return useAutoDark;
+        return useAutoDark ? ForceDarkType.FORCE_DARK : ForceDarkType.NONE;
     }
 
     private void updateForceDarkMode() {
         if (mAttachInfo.mThreadedRenderer == null) return;
-        if (mAttachInfo.mThreadedRenderer.setForceDark(isForceDarkEnabled())) {
+        if (mAttachInfo.mThreadedRenderer.setForceDark(determineForceDarkType())) {
             // TODO: Don't require regenerating all display lists to apply this setting
             invalidateWorld(mView);
         }
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index cfec081..c735142 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -17,7 +17,6 @@
 package android.view;
 
 import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT;
-import static android.view.flags.Flags.FLAG_WM_DISPLAY_REFRESH_RATE_TEST;
 import static android.view.View.STATUS_BAR_DISABLE_BACK;
 import static android.view.View.STATUS_BAR_DISABLE_CLOCK;
 import static android.view.View.STATUS_BAR_DISABLE_EXPAND;
@@ -3907,7 +3906,7 @@
          * This value is ignored if {@link #preferredDisplayModeId} is set.
          * @hide
          */
-        @FlaggedApi(FLAG_WM_DISPLAY_REFRESH_RATE_TEST)
+        @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
         @TestApi
         public float preferredMinDisplayRefreshRate;
 
@@ -3917,7 +3916,7 @@
          * This value is ignored if {@link #preferredDisplayModeId} is set.
          * @hide
          */
-        @FlaggedApi(FLAG_WM_DISPLAY_REFRESH_RATE_TEST)
+        @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
         @TestApi
         public float preferredMaxDisplayRefreshRate;
 
diff --git a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig
new file mode 100644
index 0000000..3c15518
--- /dev/null
+++ b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.view.contentcapture.flags"
+
+flag {
+    name: "run_on_background_thread_enabled"
+    namespace: "machine_learning"
+    description: "Feature flag for running content capture tasks on background thread"
+    bug: "309411951"
+}
diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index cc951cf..a467afe 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -42,11 +42,4 @@
   namespace: "core_graphics"
   description: "Enable the `setFrameRate` callback"
   bug: "299946220"
-}
-
-flag {
-    name: "wm_display_refresh_rate_test"
-    namespace: "core_graphics"
-    description: "Adds WindowManager display refresh rate fields to test API"
-    bug: "304475199"
 }
\ No newline at end of file
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 1a2d202..5bfa3d7 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -55,6 +55,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Used to communicate information about what is changing during a transition to a TransitionPlayer.
@@ -610,7 +611,7 @@
         private final WindowContainerToken mContainer;
         private WindowContainerToken mParent;
         private WindowContainerToken mLastParent;
-        private final SurfaceControl mLeash;
+        private SurfaceControl mLeash;
         private @TransitionMode int mMode = TRANSIT_NONE;
         private @ChangeFlags int mFlags = FLAG_NONE;
         private final Rect mStartAbsBounds = new Rect();
@@ -697,6 +698,11 @@
             mLastParent = lastParent;
         }
 
+        /** Sets the animation leash for controlling this change's container */
+        public void setLeash(@NonNull SurfaceControl leash) {
+            mLeash = Objects.requireNonNull(leash);
+        }
+
         /** Sets the transition mode for this change */
         public void setMode(@TransitionMode int mode) {
             mMode = mode;
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 4bfb177..94e6009 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -19,4 +19,11 @@
     namespace: "responsible_apis"
     description: "Enable toasts to indicate (potential) BAL blocking."
     bug: "308059069"
+}
+
+flag {
+    name: "bal_show_toasts_blocked"
+    namespace: "responsible_apis"
+    description: "Enable toasts to indicate actual BAL blocking."
+    bug: "308059069"
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index 08de4dfb..be3f10a 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -287,7 +287,6 @@
                         updatedView = mInflater.inflate(
                                 R.layout.app_language_picker_current_locale_item,
                                 parent, false);
-                        addStateDescriptionIntoCurrentLocaleItem(updatedView);
                     }
                 } else {
                     shouldReuseView = convertView instanceof TextView
@@ -304,7 +303,6 @@
                 if (!shouldReuseView) {
                     updatedView = mInflater.inflate(
                             R.layout.app_language_picker_current_locale_item, parent, false);
-                    addStateDescriptionIntoCurrentLocaleItem(updatedView);
                 }
                 break;
             default:
@@ -441,9 +439,4 @@
                     : View.TEXT_DIRECTION_LTR);
         }
     }
-
-    private void addStateDescriptionIntoCurrentLocaleItem(View root) {
-        String description = root.getContext().getResources().getString(R.string.checked);
-        root.setStateDescription(description);
-    }
 }
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index e014ab0..6c17e9e 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -65,6 +65,7 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.transition.Scene;
@@ -183,6 +184,12 @@
     private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet();
 
     /**
+     * Since which target SDK version this window should be edge-to-edge by default.
+     */
+    private static final int DEFAULT_EDGE_TO_EDGE_SDK_VERSION =
+            SystemProperties.getInt("persist.wm.debug.default_e2e_since_sdk", Integer.MAX_VALUE);
+
+    /**
      * Simple callback used by the context menu and its submenus. The options
      * menu submenus do not use this (their behavior is more complex).
      */
@@ -359,6 +366,8 @@
 
     boolean mDecorFitsSystemWindows = true;
 
+    private final boolean mDefaultEdgeToEdge;
+
     private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher;
 
     static class WindowManagerHolder {
@@ -377,6 +386,11 @@
         mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
         mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
+        mDefaultEdgeToEdge =
+                context.getApplicationInfo().targetSdkVersion >= DEFAULT_EDGE_TO_EDGE_SDK_VERSION;
+        if (mDefaultEdgeToEdge) {
+            mDecorFitsSystemWindows = false;
+        }
     }
 
     /**
@@ -2527,7 +2541,14 @@
         final boolean targetPreQ = targetSdk < Build.VERSION_CODES.Q;
 
         if (!mForcedStatusBarColor) {
-            mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, Color.BLACK);
+            final int statusBarCompatibleColor = context.getColor(R.color.status_bar_compatible);
+            final int statusBarDefaultColor = context.getColor(R.color.status_bar_default);
+            final int statusBarColor = a.getColor(R.styleable.Window_statusBarColor,
+                    statusBarDefaultColor);
+
+            mStatusBarColor = statusBarColor == statusBarDefaultColor && !mDefaultEdgeToEdge
+                    ? statusBarCompatibleColor
+                    : statusBarColor;
         }
         if (!mForcedNavigationBarColor) {
             final int navBarCompatibleColor = context.getColor(R.color.navigation_bar_compatible);
@@ -2541,6 +2562,7 @@
                                     && Flags.navBarTransparentByDefault())
                             && !context.getResources().getBoolean(
                                     R.bool.config_navBarDefaultTransparent)
+                            && !mDefaultEdgeToEdge
                     ? navBarCompatibleColor
                     : navBarColor;
 
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index b64771b..686e1fc 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -21,11 +21,10 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.util.ArraySet;
+import android.util.EmptyArray;
 
 import dalvik.system.VMRuntime;
 
-import libcore.util.EmptyArray;
-
 import java.io.File;
 import java.lang.reflect.Array;
 import java.util.ArrayList;
@@ -84,6 +83,38 @@
         return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen);
     }
 
+    public static byte[] newUnpaddedByteArray$ravenwood(int minLen) {
+        return new byte[minLen];
+    }
+
+    public static char[] newUnpaddedCharArray$ravenwood(int minLen) {
+        return new char[minLen];
+    }
+
+    public static int[] newUnpaddedIntArray$ravenwood(int minLen) {
+        return new int[minLen];
+    }
+
+    public static boolean[] newUnpaddedBooleanArray$ravenwood(int minLen) {
+        return new boolean[minLen];
+    }
+
+    public static long[] newUnpaddedLongArray$ravenwood(int minLen) {
+        return new long[minLen];
+    }
+
+    public static float[] newUnpaddedFloatArray$ravenwood(int minLen) {
+        return new float[minLen];
+    }
+
+    public static Object[] newUnpaddedObjectArray$ravenwood(int minLen) {
+        return new Object[minLen];
+    }
+
+    public static <T> T[] newUnpaddedArray$ravenwood(Class<T> clazz, int minLen) {
+        return (T[]) Array.newInstance(clazz, minLen);
+    }
+
     /**
      * Checks if the beginnings of two byte arrays are equal.
      *
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index b462c21..58ee2b2 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -19,12 +19,14 @@
 import static android.os.Trace.TRACE_TAG_APP;
 import static android.provider.DeviceConfig.NAMESPACE_LATENCY_TRACKER;
 
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL_UNLOCKED;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_EXPAND_PANEL;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED;
@@ -222,6 +224,16 @@
      */
     public static final int ACTION_NOTIFICATION_BIG_PICTURE_LOADED = 23;
 
+    /**
+     * Time it takes to unlock the device via udfps, until the whole launcher appears.
+     */
+    public static final int ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME = 24;
+
+    /**
+     * Time it takes to start back preview surface animation after a back gesture starts.
+     */
+    public static final int ACTION_BACK_SYSTEM_ANIMATION = 25;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -247,6 +259,8 @@
         ACTION_REQUEST_IME_HIDDEN,
         ACTION_SMARTSPACE_DOORBELL,
         ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
+        ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
+        ACTION_BACK_SYSTEM_ANIMATION,
     };
 
     /** @hide */
@@ -275,6 +289,8 @@
         ACTION_REQUEST_IME_HIDDEN,
         ACTION_SMARTSPACE_DOORBELL,
         ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
+        ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
+        ACTION_BACK_SYSTEM_ANIMATION,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -306,6 +322,8 @@
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION,
     };
 
     private final Object mLock = new Object();
@@ -492,6 +510,10 @@
                 return "ACTION_SMARTSPACE_DOORBELL";
             case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED:
                 return "ACTION_NOTIFICATION_BIG_PICTURE_LOADED";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME:
+                return "ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION:
+                return "ACTION_BACK_SYSTEM_ANIMATION";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ab0ef7d..6859f1f 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2247,6 +2247,13 @@
     <permission android:name="android.permission.MANAGE_LOWPAN_INTERFACES"
         android:protectionLevel="signature|privileged" />
 
+    <!-- @SystemApi @hide Allows changing Thread network state and access to Thread network
+        credentials such as Network Key and PSKc.
+        <p>Not for use by third-party applications.
+        @FlaggedApi("com.android.net.thread.flags.thread_enabled") -->
+    <permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"
+                android:protectionLevel="signature|privileged" />
+
     <!-- #SystemApi @hide Allows an app to bypass Private DNS.
          <p>Not for use by third-party applications.
          TODO: publish as system API in next API release. -->
@@ -7782,6 +7789,15 @@
     <permission android:name="android.permission.WRITE_FLAGS"
         android:protectionLevel="signature" />
 
+    <!-- @hide @SystemApi
+         @FlaggedApi("android.app.get_binding_uid_importance")
+         Allows to get the importance of an UID that has a service
+         binding to the app.
+         <p>Protection level: signature|privileged
+    -->
+    <permission android:name="android.permission.GET_BINDING_UID_IMPORTANCE"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @hide Allows internal applications to manage displays.
         <p>This means intercept internal signals about displays being (dis-)connected
         and being able to enable or disable the external displays.
diff --git a/core/res/res/layout/app_language_picker_current_locale_item.xml b/core/res/res/layout/app_language_picker_current_locale_item.xml
index 990e26c..01b9cc5 100644
--- a/core/res/res/layout/app_language_picker_current_locale_item.xml
+++ b/core/res/res/layout/app_language_picker_current_locale_item.xml
@@ -39,6 +39,7 @@
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:src="@drawable/ic_check_24dp"
-            app:tint="?attr/colorAccentPrimaryVariant"/>
+            app:tint="?attr/colorAccentPrimaryVariant"
+            android:contentDescription="@*android:string/checked"/>
     </LinearLayout>
 </LinearLayout>
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index c0c6e05..30beee0 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -568,6 +568,10 @@
     <color name="side_fps_button_color">#00677E</color>
 
     <!-- Color for system bars -->
+    <color name="status_bar_compatible">@android:color/black</color>
+    <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent
+         color is set by the framework or not. -->
+    <color name="status_bar_default">#00808080</color>
     <color name="navigation_bar_compatible">@android:color/black</color>
     <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent
          color is set by the framework or not. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e646548..8748ca1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3074,6 +3074,8 @@
   <java-symbol type="bool" name="config_navBarDefaultTransparent" />
   <java-symbol type="color" name="navigation_bar_default"/>
   <java-symbol type="color" name="navigation_bar_compatible"/>
+  <java-symbol type="color" name="status_bar_default"/>
+  <java-symbol type="color" name="status_bar_compatible"/>
 
   <!-- EditText suggestion popup. -->
   <java-symbol type="id" name="suggestionWindowContainer" />
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index bdbf96b..d5d67ab 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -190,7 +190,7 @@
         <item name="windowTranslucentStatus">false</item>
         <item name="windowTranslucentNavigation">false</item>
         <item name="windowDrawsSystemBarBackgrounds">false</item>
-        <item name="statusBarColor">@color/black</item>
+        <item name="statusBarColor">@color/status_bar_default</item>
         <item name="navigationBarColor">@color/navigation_bar_default</item>
         <item name="windowActionBarFullscreenDecorLayout">@layout/screen_action_bar</item>
         <item name="windowContentTransitions">false</item>
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 3a2e50a..709646b 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
          http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
 
     <!-- Arab Emirates -->
-    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214" />
+    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253" />
 
     <!-- Albania: 5 digits, known short codes listed -->
     <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -155,7 +155,7 @@
     <shortcode country="ie" pattern="\\d{5}" premium="5[3-9]\\d{3}" free="50\\d{3}|116\\d{3}" standard="5[12]\\d{3}" />
 
     <!-- Israel: 4 digits, known premium codes listed -->
-    <shortcode country="il" pattern="\\d{4}" premium="4422|4545" />
+    <shortcode country="il" pattern="\\d{1,5}" premium="4422|4545"  free="37477" />
 
     <!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU:
          https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
@@ -198,6 +198,9 @@
     <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf -->
     <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668" />
 
+    <!-- Namibia: 5 digits -->
+    <shortcode country="na" pattern="\\d{1,5}" free="40005" />
+
     <!-- The Netherlands, 4 digits, known premium codes listed, plus EU -->
     <shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" />
 
diff --git a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
index 13ce253..2323527 100644
--- a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
@@ -96,13 +96,28 @@
         assertEquals(othr, getSqlStatementType("-- cmt\n SE"));
         assertEquals(othr, getSqlStatementType("WITH"));
 
-        // Test the extended statement types.
+        // Verify that leading line-comments are skipped.
+        assertEquals(sel, getSqlStatementType("-- cmt\n SELECT"));
+        assertEquals(sel, getSqlStatementType("-- line 1\n-- line 2\n SELECT"));
+        assertEquals(sel, getSqlStatementType("-- line 1\nSELECT"));
+        // Verify that embedded comments do not confuse the scanner.
+        assertEquals(sel, getSqlStatementType("-- line 1\nSELECT\n-- line 3\n"));
 
-        final int wit = STATEMENT_WITH;
-        assertEquals(wit, getSqlStatementTypeExtended("WITH"));
+        // Verify that leading block-comments are skipped.
+        assertEquals(sel, getSqlStatementType("/* foo */SELECT"));
+        assertEquals(sel, getSqlStatementType("/* line 1\n line 2\n*/\nSELECT"));
+        assertEquals(sel, getSqlStatementType("/* UPDATE\nline 2*/\nSELECT"));
+        // Verify that embedded comment characters do not confuse the scanner.
+        assertEquals(sel, getSqlStatementType("/* Foo /* /* // ** */SELECT"));
 
-        final int cmt = STATEMENT_COMMENT;
-        assertEquals(cmt, getSqlStatementTypeExtended("-- cmt\n SELECT"));
+        // Mix it up with comment types
+        assertEquals(sel, getSqlStatementType("/* foo */ -- bar\n SELECT"));
+
+        // Test the extended statement types.  Note that the STATEMENT_COMMENT type is not possible,
+        // since leading comments are skipped.
+
+        final int with = STATEMENT_WITH;
+        assertEquals(with, getSqlStatementTypeExtended("WITH"));
 
         final int cre = STATEMENT_CREATE;
         assertEquals(cre, getSqlStatementTypeExtended("CREATE TABLE t1 (i int)"));
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index 4ee987b..3fc08ee 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -39,9 +39,13 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Phaser;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
@@ -231,4 +235,116 @@
             // This exception is expected.
         }
     }
+
+    /**
+     * Count the number of rows in the database <count> times.  The answer must match <expected>
+     * every time.  Any errors are reported back to the main thread through the <errors>
+     * array. The ticker forces the database reads to be interleaved with database operations from
+     * the sibling threads.
+     */
+    private void concurrentReadOnlyReader(SQLiteDatabase database, int count, long expected,
+            List<Throwable> errors, Phaser ticker) {
+
+        final String query = "--comment\nSELECT count(*) from t1";
+
+        try {
+            for (int i = count; i > 0; i--) {
+                ticker.arriveAndAwaitAdvance();
+                long r = DatabaseUtils.longForQuery(database, query, null);
+                if (r != expected) {
+                    // The type of the exception is not important.  Only the message matters.
+                    throw new RuntimeException(
+                        String.format("concurrentRead expected %d, got %d", expected, r));
+                }
+            }
+        } catch (Throwable t) {
+            errors.add(t);
+        } finally {
+            ticker.arriveAndDeregister();
+        }
+    }
+
+    /**
+     * Insert a new row <count> times.  Any errors are reported back to the main thread through
+     * the <errors> array. The ticker forces the database reads to be interleaved with database
+     * operations from the sibling threads.
+     */
+    private void concurrentImmediateWriter(SQLiteDatabase database, int count,
+            List<Throwable> errors, Phaser ticker) {
+        database.beginTransaction();
+        try {
+            int n = 100;
+            for (int i = count; i > 0; i--) {
+                ticker.arriveAndAwaitAdvance();
+                database.execSQL(String.format("INSERT INTO t1 (i) VALUES (%d)", n++));
+            }
+            database.setTransactionSuccessful();
+        } catch (Throwable t) {
+            errors.add(t);
+        } finally {
+            database.endTransaction();
+            ticker.arriveAndDeregister();
+        }
+    }
+
+    /**
+     * This test verifies that a read-only transaction can be started, and it is deferred.  A
+     * deferred transaction does not take a database locks until the database is accessed.  This
+     * test verifies that the implicit connection selection process correctly identifies
+     * read-only transactions even when they are preceded by a comment.
+     */
+    @Test
+    public void testReadOnlyTransaction() throws Exception {
+        // Enable WAL for concurrent read and write transactions.
+        mDatabase.enableWriteAheadLogging();
+
+        // Create the t1 table and put some data in it.
+        mDatabase.beginTransaction();
+        try {
+            mDatabase.execSQL("CREATE TABLE t1 (i int);");
+            mDatabase.execSQL("INSERT INTO t1 (i) VALUES (2)");
+            mDatabase.execSQL("INSERT INTO t1 (i) VALUES (3)");
+            mDatabase.setTransactionSuccessful();
+        } finally {
+            mDatabase.endTransaction();
+        }
+
+        // Threads install errors in this array.
+        final List<Throwable> errors = Collections.synchronizedList(new ArrayList<Throwable>());
+
+        // This forces the read and write threads to execute in a lock-step, round-robin fashion.
+        Phaser ticker = new Phaser(3);
+
+        // Create three threads that will perform transactions.  One thread is a writer and two
+        // are readers.  The intent is that the readers begin before the writer commits, so the
+        // readers always see a database with two rows.
+        Thread readerA = new Thread(() -> {
+              concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker);
+        });
+        Thread readerB = new Thread(() -> {
+              concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker);
+        });
+        Thread writerC = new Thread(() -> {
+              concurrentImmediateWriter(mDatabase, 4, errors, ticker);
+        });
+
+        readerA.start();
+        readerB.start();
+        writerC.start();
+
+        // All three threads should have completed.  Give the total set 1s.  The 10ms delay for
+        // the second and third threads is just a small, positive number.
+        readerA.join(1000);
+        assertFalse(readerA.isAlive());
+        readerB.join(10);
+        assertFalse(readerB.isAlive());
+        writerC.join(10);
+        assertFalse(writerC.isAlive());
+
+        // The writer added 4 rows to the database.
+        long r = DatabaseUtils.longForQuery(mDatabase, "SELECT count(*) from t1", null);
+        assertEquals(6, r);
+
+        assertTrue("ReadThread failed with errors: " + errors, errors.isEmpty());
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index e7117a7..dfe6cf8 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -49,6 +49,7 @@
 import android.app.Instrumentation;
 import android.app.UiModeManager;
 import android.content.Context;
+import android.graphics.ForceDarkType;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.Binder;
 import android.os.SystemProperties;
@@ -593,7 +594,7 @@
                 mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
         );
 
-        assertThat(mViewRootImpl.isForceDarkEnabled()).isFalse();
+        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
     }
 
     @Test
@@ -613,7 +614,8 @@
                 mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
         );
 
-        assertThat(mViewRootImpl.isForceDarkEnabled()).isTrue();
+        assertThat(mViewRootImpl.determineForceDarkType())
+                .isEqualTo(ForceDarkType.FORCE_INVERT_COLOR_DARK);
     }
 
     @Test
@@ -634,7 +636,7 @@
                 mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
         );
 
-        assertThat(mViewRootImpl.isForceDarkEnabled()).isFalse();
+        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
     }
 
     @Test
@@ -654,7 +656,7 @@
                 mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
         );
 
-        assertThat(mViewRootImpl.isForceDarkEnabled()).isTrue();
+        assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.FORCE_DARK);
     }
 
     private boolean setForceDarkSysProp(boolean isForceDarkEnabled) {
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 69aa401..32186667 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -439,6 +439,8 @@
         <permission name="android.permission.MANAGE_WIFI_NETWORK_SELECTION" />
         <!-- Permission needed for CTS test - ConcurrencyTest#testP2pSetWfdInfo -->
         <permission name="android.permission.CONFIGURE_WIFI_DISPLAY" />
+        <!-- Permission required for CTS test - CtsThreadNetworkTestCases -->
+        <permission name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
         <!-- Permission required for CTS test CarrierMessagingServiceWrapperTest -->
         <permission name="android.permission.BIND_CARRIER_SERVICES"/>
         <!-- Permission required for CTS test - MusicRecognitionManagerTest -->
@@ -529,6 +531,7 @@
         <permission name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"/>
         <!-- Permission required for CTS test IntentRedirectionTest -->
         <permission name="android.permission.QUERY_CLONED_APPS"/>
+        <permission name="android.permission.GET_BINDING_UID_IMPORTANCE"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/framework-minus-apex-ravenwood-policies.txt b/framework-minus-apex-ravenwood-policies.txt
index c3ca122..8e76fd2 100644
--- a/framework-minus-apex-ravenwood-policies.txt
+++ b/framework-minus-apex-ravenwood-policies.txt
@@ -1,11 +1,91 @@
 # Ravenwood "policy" file for framework-minus-apex.
 
+# Collections
 class android.util.ArrayMap stubclass
+class android.util.ArraySet stubclass
+class android.util.LongSparseArray stubclass
+class android.util.SparseArrayMap stubclass
+class android.util.SparseArray stubclass
+class android.util.SparseBooleanArray stubclass
+class android.util.SparseIntArray stubclass
+class android.util.SparseLongArray stubclass
 class android.util.ContainerHelpers stubclass
 class android.util.EmptyArray stubclass
 class android.util.MapCollections stubclass
 
+# Logging
 class android.util.Log stubclass
 class android.util.Log !com.android.hoststubgen.nativesubstitution.Log_host
+class android.util.LogPrinter stubclass
 
+# String Manipulation
+class android.util.Printer stubclass
+class android.util.PrintStreamPrinter stubclass
+class android.util.PrintWriterPrinter stubclass
+class android.util.StringBuilderPrinter stubclass
+
+# Properties
+class android.util.Property stubclass
+class android.util.FloatProperty stubclass
+class android.util.IntProperty stubclass
+class android.util.NoSuchPropertyException stubclass
+class android.util.ReflectiveProperty stubclass
+
+# Exceptions
+class android.util.AndroidException stubclass
+class android.util.AndroidRuntimeException stubclass
+
+# JSON
+class android.util.JsonReader stubclass
+class android.util.JsonWriter stubclass
+class android.util.MalformedJsonException stubclass
+
+# Base64
+class android.util.Base64 stubclass
+class android.util.Base64DataException stubclass
+class android.util.Base64InputStream stubclass
+class android.util.Base64OutputStream stubclass
+
+# Data Holders
+class android.util.MutableFloat stubclass
+class android.util.MutableShort stubclass
+class android.util.MutableBoolean stubclass
+class android.util.MutableByte stubclass
+class android.util.MutableChar stubclass
+class android.util.MutableDouble stubclass
+class android.util.Pair stubclass
+class android.util.Range stubclass
+class android.util.Rational stubclass
+class android.util.Size stubclass
+class android.util.SizeF stubclass
+
+# Proto
+class android.util.proto.EncodedBuffer stubclass
+class android.util.proto.ProtoInputStream stubclass
+class android.util.proto.ProtoOutputStream stubclass
+class android.util.proto.ProtoParseException stubclass
+class android.util.proto.ProtoStream stubclass
+class android.util.proto.ProtoUtils stubclass
+class android.util.proto.WireTypeMismatchException stubclass
+
+# Misc
+class android.util.Dumpable stubclass
+class android.util.DebugUtils stubclass
+class android.util.UtilConfig stubclass
+class android.util.Patterns stubclass
+
+# Internals
+class com.android.internal.util.ArrayUtils stubclass
+    method newUnpaddedByteArray (I)[B @newUnpaddedByteArray$ravenwood
+    method newUnpaddedCharArray (I)[C @newUnpaddedCharArray$ravenwood
+    method newUnpaddedIntArray (I)[I @newUnpaddedIntArray$ravenwood
+    method newUnpaddedBooleanArray (I)[Z @newUnpaddedBooleanArray$ravenwood
+    method newUnpaddedLongArray (I)[J @newUnpaddedLongArray$ravenwood
+    method newUnpaddedFloatArray (I)[F @newUnpaddedFloatArray$ravenwood
+    method newUnpaddedObjectArray (I)[Ljava/lang/Object; @newUnpaddedObjectArray$ravenwood
+    method newUnpaddedArray (Ljava/lang/Class;I)[Ljava/lang/Object; @newUnpaddedArray$ravenwood
+
+class com.android.internal.util.GrowingArrayUtils stubclass
 class com.android.internal.util.LineBreakBufferedWriter stubclass
+class com.android.internal.util.Preconditions stubclass
+class com.android.internal.util.StringPool stubclass
diff --git a/graphics/java/android/graphics/ForceDarkType.java b/graphics/java/android/graphics/ForceDarkType.java
new file mode 100644
index 0000000..396b037
--- /dev/null
+++ b/graphics/java/android/graphics/ForceDarkType.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The style of force dark to use in {@link HardwareRenderer}.
+ *
+ * You must keep this in sync with the C++ enum ForceDarkType in
+ * frameworks/base/libs/hwui/utils/ForceDark.h
+ *
+ * @hide
+ */
+public class ForceDarkType {
+    /**
+     * Force dark disabled: normal, default operation.
+     *
+     * @hide
+     */
+    public static final int NONE = 0;
+
+    /**
+     * Use force dark
+     * @hide
+     */
+    public static final int FORCE_DARK = 1;
+
+    /**
+     * Force force-dark. {@see Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED}
+     * @hide */
+    public static final int FORCE_INVERT_COLOR_DARK = 2;
+
+    /** @hide */
+    @IntDef({
+        NONE,
+        FORCE_DARK,
+        FORCE_INVERT_COLOR_DARK,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ForceDarkTypeDef {}
+
+}
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index 8cd262e..20e393e 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -182,7 +182,7 @@
     /** @hide */
     protected RenderNode mRootNode;
     private boolean mOpaque = true;
-    private boolean mForceDark = false;
+    private int mForceDark = ForceDarkType.NONE;
     private @ActivityInfo.ColorMode int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT;
     private float mDesiredSdrHdrRatio = 1f;
 
@@ -571,10 +571,10 @@
      * Whether or not the force-dark feature should be used for this renderer.
      * @hide
      */
-    public boolean setForceDark(boolean enable) {
-        if (mForceDark != enable) {
-            mForceDark = enable;
-            nSetForceDark(mNativeProxy, enable);
+    public boolean setForceDark(@ForceDarkType.ForceDarkTypeDef int type) {
+        if (mForceDark != type) {
+            mForceDark = type;
+            nSetForceDark(mNativeProxy, type);
             return true;
         }
         return false;
@@ -1597,7 +1597,7 @@
 
     private static native void nAllocateBuffers(long nativeProxy);
 
-    private static native void nSetForceDark(long nativeProxy, boolean enabled);
+    private static native void nSetForceDark(long nativeProxy, int type);
 
     private static native void nSetDisplayDensityDpi(int densityDpi);
 
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 0e59e9a..29bdd5c 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -28,3 +28,10 @@
     description: "Enables invoking split contextually"
     bug: "276361926"
 }
+
+flag {
+    name: "enable_taskbar_navbar_unification"
+    namespace: "multitasking"
+    description: "Enables taskbar / navbar unification"
+    bug: "309671494"
+}
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
index d2360e9..657720e 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml
@@ -21,7 +21,7 @@
     <solid
         android:color="?androidprv:attr/materialColorSurfaceContainerHigh"
         />
-    <corners android:radius="20dp" />
+    <corners android:radius="18sp" />
 
     <padding
         android:left="20dp"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 55a9132..de9d2a2 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -185,10 +185,11 @@
     <dimen name="bubble_pointer_overlap">1dp</dimen>
     <!-- Extra padding around the dismiss target for bubbles -->
     <dimen name="bubble_dismiss_slop">16dp</dimen>
-    <!-- Height of button allowing users to adjust settings for bubbles. -->
-    <dimen name="bubble_manage_button_height">36dp</dimen>
-    <!-- Height of manage button including margins. -->
-    <dimen name="bubble_manage_button_total_height">68dp</dimen>
+    <!-- Height of button allowing users to adjust settings for bubbles. We use sp so that the
+         button can scale with the font size. -->
+    <dimen name="bubble_manage_button_height">36sp</dimen>
+    <!-- Touch area height of the manage button. -->
+    <dimen name="bubble_manage_button_touch_area_height">48dp</dimen>
     <!-- The margin around the outside of the manage button. -->
     <dimen name="bubble_manage_button_margin">16dp</dimen>
     <!-- Height of an item in the bubble manage menu. -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 5cf9175..8241e1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -136,6 +136,7 @@
 
     /** Called on frame update. */
     final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+        mTransformation.clear();
         // Extract the transformation to the current time.
         mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
                 mTransformation);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
index 880579d..2ec9e8b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
@@ -88,8 +88,6 @@
     public static final PathInterpolator DIM_INTERPOLATOR =
             new PathInterpolator(.23f, .87f, .52f, -0.11f);
 
-    public static final Interpolator DECELERATE = new PathInterpolator(0f, 0f, 0.5f, 1f);
-
     // Create the default emphasized interpolator
     private static PathInterpolator createEmphasizedInterpolator() {
         Path path = new Path();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 03c546d..5843635 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -61,6 +61,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.LatencyTracker;
 import com.android.internal.view.AppearanceRegion;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
@@ -99,6 +100,7 @@
      * Max duration to wait for an animation to finish before triggering the real back.
      */
     private static final long MAX_ANIMATION_DURATION = 2000;
+    private final LatencyTracker mLatencyTracker;
 
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
@@ -167,6 +169,7 @@
 
     private final BackAnimationBackground mAnimationBackground;
     private StatusBarCustomizer mCustomizer;
+    private boolean mTrackingLatency;
 
     public BackAnimationController(
             @NonNull ShellInit shellInit,
@@ -213,6 +216,7 @@
                 .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
                 .build();
         mShellBackAnimationRegistry = shellBackAnimationRegistry;
+        mLatencyTracker = LatencyTracker.getInstance(mContext);
     }
 
     private void onInit() {
@@ -438,6 +442,7 @@
 
     private void startBackNavigation(@NonNull TouchTracker touchTracker) {
         try {
+            startLatencyTracking();
             mBackNavigationInfo = mActivityTaskManager.startBackNavigation(
                     mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null);
             onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker);
@@ -452,6 +457,7 @@
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
         if (backNavigationInfo == null) {
             ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
+            cancelLatencyTracking();
             return;
         }
         final int backType = backNavigationInfo.getType();
@@ -462,6 +468,8 @@
             }
         } else {
             mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+            // App is handling back animation. Cancel system animation latency tracking.
+            cancelLatencyTracking();
             dispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
         }
     }
@@ -808,12 +816,36 @@
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
         mActiveCallback = null;
         mShellBackAnimationRegistry.resetDefaultCrossActivity();
+        cancelLatencyTracking();
         if (mBackNavigationInfo != null) {
             mBackNavigationInfo.onBackNavigationFinished(triggerBack);
             mBackNavigationInfo = null;
         }
     }
 
+    private void startLatencyTracking() {
+        if (mTrackingLatency) {
+            cancelLatencyTracking();
+        }
+        mLatencyTracker.onActionStart(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
+        mTrackingLatency = true;
+    }
+
+    private void cancelLatencyTracking() {
+        if (!mTrackingLatency) {
+            return;
+        }
+        mLatencyTracker.onActionCancel(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
+        mTrackingLatency = false;
+    }
+
+    private void endLatencyTracking() {
+        if (!mTrackingLatency) {
+            return;
+        }
+        mLatencyTracker.onActionEnd(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
+        mTrackingLatency = false;
+    }
 
     private void createAdapter() {
         IBackAnimationRunner runner =
@@ -826,6 +858,7 @@
                             IBackAnimationFinishedCallback finishedCallback) {
                         mShellExecutor.execute(
                                 () -> {
+                                    endLatencyTracking();
                                     if (mBackNavigationInfo == null) {
                                         ProtoLog.e(WM_SHELL_BACK_PREVIEW,
                                                 "Lack of navigation info to start animation.");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 8ec297e..5a15674 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -89,7 +89,6 @@
 
     private final PointF mInitialTouchPos = new PointF();
     private final Interpolator mPostAnimationInterpolator = Interpolators.EMPHASIZED;
-    private final Interpolator mYMovementInterpolator = Interpolators.DECELERATE;
     private final Interpolator mProgressInterpolator = new DecelerateInterpolator();
     private final Matrix mTransformMatrix = new Matrix();
 
@@ -164,7 +163,7 @@
         float yDirection = rawYDelta < 0 ? -1 : 1;
         // limit yDelta interpretation to 1/2 of screen height in either direction
         float deltaYRatio = Math.min(height / 2f, Math.abs(rawYDelta)) / (height / 2f);
-        float interpolatedYRatio = mYMovementInterpolator.getInterpolation(deltaYRatio);
+        float interpolatedYRatio = mProgressInterpolator.getInterpolation(deltaYRatio);
         // limit y-shift so surface never passes 8dp screen margin
         float deltaY = yDirection * interpolatedYRatio * Math.max(0f,
                 (height - scaledHeight) / 2f - mVerticalMargin);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 0568eda..c7ab6aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -57,6 +57,7 @@
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
@@ -470,6 +471,17 @@
                     R.layout.bubble_manage_button, this /* parent */, false /* attach */);
             addView(mManageButton);
             mManageButton.setVisibility(visibility);
+            post(() -> {
+                int touchAreaHeight =
+                        getResources().getDimensionPixelSize(
+                                R.dimen.bubble_manage_button_touch_area_height);
+                Rect r = new Rect();
+                mManageButton.getHitRect(r);
+                int extraTouchArea = (touchAreaHeight - r.height()) / 2;
+                r.top -= extraTouchArea;
+                r.bottom += extraTouchArea;
+                setTouchDelegate(new TouchDelegate(r, mManageButton));
+            });
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 17e06e9..144c456 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -198,9 +198,10 @@
         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
         mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
-        mManageButtonHeightIncludingMargins =
-                res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height);
         mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height);
+        mManageButtonHeightIncludingMargins =
+                mManageButtonHeight
+                + 2 * res.getDimensionPixelSize(R.dimen.bubble_manage_button_margin);
         mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
         mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
         mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 9402d02..87461dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1829,9 +1829,12 @@
                 }
                 bubble.cleanupViews(); // cleans up the icon view
                 updateExpandedView(); // resets state for no expanded bubble
+                mExpandedBubble = null;
             });
             logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
             return;
+        } else if (getBubbleCount() == 1) {
+            mExpandedBubble = null;
         }
         // Remove it from the views
         for (int i = 0; i < getBubbleCount(); i++) {
@@ -2420,14 +2423,13 @@
         mExpandedAnimationController.notifyPreparingToCollapse();
 
         updateOverflowDotVisibility(false /* expanding */);
-        final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack(
-                mStackAnimationController
-                        .getStackPositionAlongNearestHorizontalEdge()
-                /* collapseTo */,
-                () -> {
-                    mBubbleContainer.setActiveController(mStackAnimationController);
-                    updateOverflowVisibility();
-                });
+        final Runnable collapseBackToStack = () ->
+                mExpandedAnimationController.collapseBackToStack(
+                        mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
+                        () -> {
+                            mBubbleContainer.setActiveController(mStackAnimationController);
+                            updateOverflowVisibility();
+                        });
 
         final Runnable after = () -> {
             final BubbleViewProvider previouslySelected = mExpandedBubble;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 64294c9..54cf84c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -646,11 +646,12 @@
     @Provides
     static KeyguardTransitionHandler provideKeyguardTransitionHandler(
             ShellInit shellInit,
+            ShellController shellController,
             Transitions transitions,
             @ShellMainThread Handler mainHandler,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new KeyguardTransitionHandler(
-                    shellInit, transitions, mainHandler, mainExecutor);
+                    shellInit, shellController, transitions, mainHandler, mainExecutor);
     }
 
     @WMSingleton
@@ -837,10 +838,12 @@
         // Use optional-of-lazy for the dependency that this provider relies on.
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
-        if (DesktopModeStatus.isEnabled()) {
-            return desktopTasksController.map(Lazy::get);
-        }
-        return Optional.empty();
+        return desktopTasksController.flatMap((lazy)-> {
+            if (DesktopModeStatus.isEnabled()) {
+                return Optional.of(lazy.get());
+            }
+            return Optional.empty();
+        });
     }
 
     @BindsOptionalOf
@@ -854,10 +857,12 @@
         // Use optional-of-lazy for the dependency that this provider relies on.
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
-        if (DesktopModeStatus.isEnabled()) {
-            return desktopModeTaskRepository.map(Lazy::get);
-        }
-        return Optional.empty();
+        return desktopModeTaskRepository.flatMap((lazy)-> {
+            if (DesktopModeStatus.isEnabled()) {
+                return Optional.of(lazy.get());
+            }
+            return Optional.empty();
+        });
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index dba7f4b..0890861 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -50,6 +50,8 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.KeyguardChangeListener;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback;
@@ -59,10 +61,12 @@
  *
  * <p>This takes the highest priority.
  */
-public class KeyguardTransitionHandler implements Transitions.TransitionHandler {
+public class KeyguardTransitionHandler
+        implements Transitions.TransitionHandler, KeyguardChangeListener {
     private static final String TAG = "KeyguardTransition";
 
     private final Transitions mTransitions;
+    private final ShellController mShellController;
     private final Handler mMainHandler;
     private final ShellExecutor mMainExecutor;
 
@@ -81,6 +85,9 @@
     // transition.
     private boolean mIsLaunchingActivityOverLockscreen;
 
+    // Last value reported by {@link KeyguardChangeListener}.
+    private boolean mKeyguardShowing = true;
+
     private final class StartedTransition {
         final TransitionInfo mInfo;
         final SurfaceControl.Transaction mFinishT;
@@ -93,12 +100,15 @@
             mPlayer = player;
         }
     }
+
     public KeyguardTransitionHandler(
             @NonNull ShellInit shellInit,
+            @NonNull ShellController shellController,
             @NonNull Transitions transitions,
             @NonNull Handler mainHandler,
             @NonNull ShellExecutor mainExecutor) {
         mTransitions = transitions;
+        mShellController = shellController;
         mMainHandler = mainHandler;
         mMainExecutor = mainExecutor;
         shellInit.addInitCallback(this::onInit, this);
@@ -106,6 +116,7 @@
 
     private void onInit() {
         mTransitions.addHandler(this);
+        mShellController.addKeyguardChangeListener(this);
     }
 
     /**
@@ -121,6 +132,16 @@
     }
 
     @Override
+    public void onKeyguardVisibilityChanged(
+            boolean visible, boolean occluded, boolean animatingDismiss) {
+        mKeyguardShowing = visible;
+    }
+
+    public boolean isKeyguardShowing() {
+        return mKeyguardShowing;
+    }
+
+    @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index c20d23e..271a3b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -64,6 +64,7 @@
 import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
+import java.util.function.Consumer;
 
 /**
  * Handles the Recents (overview) animation. Only one of these can run at a time. A recents
@@ -130,21 +131,21 @@
         wct.sendPendingIntent(intent, fillIn, options);
         final RecentsController controller = new RecentsController(listener);
         RecentsMixedHandler mixer = null;
-        Transitions.TransitionHandler mixedHandler = null;
+        Consumer<IBinder> setTransitionForMixer = null;
         for (int i = 0; i < mMixers.size(); ++i) {
-            mixedHandler = mMixers.get(i).handleRecentsRequest(wct);
-            if (mixedHandler != null) {
+            setTransitionForMixer = mMixers.get(i).handleRecentsRequest(wct);
+            if (setTransitionForMixer != null) {
                 mixer = mMixers.get(i);
                 break;
             }
         }
         final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
-                mixedHandler == null ? this : mixedHandler);
+                mixer == null ? this : mixer);
         for (int i = 0; i < mStateListeners.size(); i++) {
             mStateListeners.get(i).onTransitionStarted(transition);
         }
         if (mixer != null) {
-            mixer.setRecentsTransition(transition);
+            setTransitionForMixer.accept(transition);
         }
         if (transition != null) {
             controller.setTransition(transition);
@@ -589,6 +590,13 @@
                 cancel("transit_sleep");
                 return;
             }
+            if (mKeyguardLocked) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                        "[%d] RecentsController.merge: keyguard is locked", mInstanceId);
+                // We will not accept new changes if we are swiping over the keyguard.
+                cancel(true /* toHome */, false /* withScreenshots */, "keyguard_locked");
+                return;
+            }
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "[%d] RecentsController.merge", mInstanceId);
             // Keep all tasks in one list because order matters.
@@ -1105,22 +1113,17 @@
      * An interface for a mixed handler to receive information about recents requests (since these
      * come into this handler directly vs from WMCore request).
      */
-    public interface RecentsMixedHandler {
+    public interface RecentsMixedHandler extends Transitions.TransitionHandler {
         /**
          * Called when a recents request comes in. The handler can add operations to outWCT. If
-         * the handler wants to "accept" the transition, it should return itself; otherwise, it
-         * should return `null`.
+         * the handler wants to "accept" the transition, it should return a Consumer accepting the
+         * IBinder for the transition. If not, it should return `null`.
          *
          * If a mixed-handler accepts this recents, it will be the de-facto handler for this
          * transition and is required to call the associated {@link #startAnimation},
          * {@link #mergeAnimation}, and {@link #onTransitionConsumed} methods.
          */
-        Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT);
-
-        /**
-         * Reports the transition token associated with the accepted recents request. If there was
-         * a problem starting the request, this will be called with `null`.
-         */
-        void setRecentsTransition(@Nullable IBinder transition);
+        @Nullable
+        Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 918a5a4..ce7fef2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -23,6 +23,7 @@
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
@@ -37,11 +38,13 @@
 import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.os.IBinder;
+import android.util.ArrayMap;
 import android.util.Pair;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -61,7 +64,9 @@
 import com.android.wm.shell.util.TransitionUtil;
 
 import java.util.ArrayList;
+import java.util.Map;
 import java.util.Optional;
+import java.util.function.Consumer;
 
 /**
  * A handler for dealing with transitions involving multiple other handlers. For example: an
@@ -79,7 +84,7 @@
     private UnfoldTransitionHandler mUnfoldHandler;
     private ActivityEmbeddingController mActivityEmbeddingController;
 
-    private static class MixedTransition {
+    private class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
         /** Both the display and split-state (enter/exit) is changing */
@@ -94,14 +99,17 @@
         /** Keyguard exit/occlude/unocclude transition. */
         static final int TYPE_KEYGUARD = 5;
 
+        /** Recents transition on top of the lock screen. */
+        static final int TYPE_RECENTS_DURING_KEYGUARD = 6;
+
         /** Recents Transition while in desktop mode. */
-        static final int TYPE_RECENTS_DURING_DESKTOP = 6;
+        static final int TYPE_RECENTS_DURING_DESKTOP = 7;
 
         /** Fold/Unfold transition. */
-        static final int TYPE_UNFOLD = 7;
+        static final int TYPE_UNFOLD = 8;
 
         /** Enter pip from one of the Activity Embedding windows. */
-        static final int TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING = 8;
+        static final int TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING = 9;
 
         /** The default animation for this mixed transition. */
         static final int ANIM_TYPE_DEFAULT = 0;
@@ -117,7 +125,10 @@
         final IBinder mTransition;
 
         Transitions.TransitionHandler mLeftoversHandler = null;
+        TransitionInfo mInfo = null;
         WindowContainerTransaction mFinishWCT = null;
+        SurfaceControl.Transaction mFinishT = null;
+        Transitions.TransitionFinishCallback mFinishCB = null;
 
         /**
          * Whether the transition has request for remote transition while mLeftoversHandler
@@ -138,6 +149,37 @@
             mTransition = transition;
         }
 
+        boolean startSubAnimation(Transitions.TransitionHandler handler, TransitionInfo info,
+                SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+            if (mInfo != null) {
+                ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                        "startSubAnimation #%d.%d", mInfo.getDebugId(), info.getDebugId());
+            }
+            mInFlightSubAnimations++;
+            if (!handler.startAnimation(
+                    mTransition, info, startT, finishT, wct -> onSubAnimationFinished(info, wct))) {
+                mInFlightSubAnimations--;
+                return false;
+            }
+            return true;
+        }
+
+        void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
+            mInFlightSubAnimations--;
+            if (mInfo != null) {
+                ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                        "onSubAnimationFinished #%d.%d remaining=%d",
+                        mInfo.getDebugId(), info.getDebugId(), mInFlightSubAnimations);
+            }
+
+            joinFinishArgs(wct);
+
+            if (mInFlightSubAnimations == 0) {
+                mActiveTransitions.remove(MixedTransition.this);
+                mFinishCB.onTransitionFinished(mFinishWCT);
+            }
+        }
+
         void joinFinishArgs(WindowContainerTransaction wct) {
             if (wct != null) {
                 if (mFinishWCT == null) {
@@ -271,39 +313,46 @@
     }
 
     @Override
-    public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) {
+    public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) {
         if (mRecentsHandler != null) {
             if (mSplitHandler.isSplitScreenVisible()) {
-                return this;
+                return this::setRecentsTransitionDuringSplit;
+            } else if (mKeyguardHandler.isKeyguardShowing()) {
+                return this::setRecentsTransitionDuringKeyguard;
             } else if (mDesktopTasksController != null
                     // Check on the default display. Recents/gesture nav is only available there
                     && mDesktopTasksController.getVisibleTaskCount(DEFAULT_DISPLAY) > 0) {
-                return this;
+                return this::setRecentsTransitionDuringDesktop;
             }
         }
         return null;
     }
 
-    @Override
-    public void setRecentsTransition(IBinder transition) {
-        if (mSplitHandler.isSplitScreenVisible()) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
-                    + "Split-Screen is foreground, so treat it as Mixed.");
-            final MixedTransition mixed = new MixedTransition(
-                    MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
-            mixed.mLeftoversHandler = mRecentsHandler;
-            mActiveTransitions.add(mixed);
-        } else if (DesktopModeStatus.isEnabled()) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
-                    + "desktop mode is active, so treat it as Mixed.");
-            final MixedTransition mixed = new MixedTransition(
-                    MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition);
-            mixed.mLeftoversHandler = mRecentsHandler;
-            mActiveTransitions.add(mixed);
-        } else {
-            throw new IllegalStateException("Accepted a recents transition but don't know how to"
-                    + " handle it");
-        }
+    private void setRecentsTransitionDuringSplit(IBinder transition) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+                + "Split-Screen is foreground, so treat it as Mixed.");
+        final MixedTransition mixed = new MixedTransition(
+                MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
+        mixed.mLeftoversHandler = mRecentsHandler;
+        mActiveTransitions.add(mixed);
+    }
+
+    private void setRecentsTransitionDuringKeyguard(IBinder transition) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+                + "keyguard is visible, so treat it as Mixed.");
+        final MixedTransition mixed = new MixedTransition(
+                MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition);
+        mixed.mLeftoversHandler = mRecentsHandler;
+        mActiveTransitions.add(mixed);
+    }
+
+    private void setRecentsTransitionDuringDesktop(IBinder transition) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+                + "desktop mode is active, so treat it as Mixed.");
+        final MixedTransition mixed = new MixedTransition(
+                MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition);
+        mixed.mLeftoversHandler = mRecentsHandler;
+        mActiveTransitions.add(mixed);
     }
 
     private TransitionInfo subCopy(@NonNull TransitionInfo info,
@@ -410,6 +459,9 @@
         } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
             return animateKeyguard(mixed, info, startTransaction, finishTransaction,
                     finishCallback);
+        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) {
+            return animateRecentsDuringKeyguard(mixed, info, startTransaction, finishTransaction,
+                    finishCallback);
         } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
             return animateRecentsDuringDesktop(mixed, info, startTransaction, finishTransaction,
                     finishCallback);
@@ -764,24 +816,28 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            mixed.mInFlightSubAnimations--;
-            if (mixed.mInFlightSubAnimations == 0) {
-                mActiveTransitions.remove(mixed);
-                finishCallback.onTransitionFinished(wct);
-            }
-        };
-        mixed.mInFlightSubAnimations++;
+        if (mixed.mFinishT == null) {
+            mixed.mFinishT = finishTransaction;
+            mixed.mFinishCB = finishCallback;
+        }
         // Sync pip state.
         if (mPipHandler != null) {
             mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
         }
-        if (!mKeyguardHandler.startAnimation(
-                mixed.mTransition, info, startTransaction, finishTransaction, finishCB)) {
-            mixed.mInFlightSubAnimations--;
-            return false;
+        return mixed.startSubAnimation(mKeyguardHandler, info, startTransaction, finishTransaction);
+    }
+
+    private boolean animateRecentsDuringKeyguard(@NonNull final MixedTransition mixed,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (mixed.mInfo == null) {
+            mixed.mInfo = info;
+            mixed.mFinishT = finishTransaction;
+            mixed.mFinishCB = finishCallback;
         }
-        return true;
+        return mixed.startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
     }
 
     private boolean animateRecentsDuringDesktop(@NonNull final MixedTransition mixed,
@@ -905,6 +961,15 @@
                         finishCallback);
             } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
                 mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+            } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) {
+                if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
+                    handoverTransitionLeashes(mixed, info, t, mixed.mFinishT);
+                    if (animateKeyguard(mixed, info, t, mixed.mFinishT, mixed.mFinishCB)) {
+                        finishCallback.onTransitionFinished(null);
+                    }
+                }
+                mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+                        finishCallback);
             } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
                 mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
                         finishCallback);
@@ -947,4 +1012,38 @@
             mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
         }
     }
+
+    /**
+     * Update an incoming {@link TransitionInfo} with the leashes from an ongoing
+     * {@link MixedTransition} so that it can take over some parts of the animation without
+     * reparenting to new transition roots.
+     */
+    private static void handoverTransitionLeashes(@NonNull MixedTransition mixed,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startT,
+            @NonNull SurfaceControl.Transaction finishT) {
+
+        // Show the roots in case they contain new changes not present in the original transition.
+        for (int j = info.getRootCount() - 1; j >= 0; --j) {
+            startT.show(info.getRoot(j).getLeash());
+        }
+
+        // Find all of the leashes from the original transition.
+        Map<WindowContainerToken, TransitionInfo.Change> originalChanges = new ArrayMap<>();
+        for (TransitionInfo.Change oldChange : mixed.mInfo.getChanges()) {
+            if (oldChange.getContainer() != null) {
+                originalChanges.put(oldChange.getContainer(), oldChange);
+            }
+        }
+
+        // Merge the animation leashes by re-using the original ones if we see the same container
+        // in the new transition and the old.
+        for (TransitionInfo.Change newChange : info.getChanges()) {
+            if (originalChanges.containsKey(newChange.getContainer())) {
+                final TransitionInfo.Change oldChange = originalChanges.get(newChange.getContainer());
+                startT.reparent(newChange.getLeash(), null);
+                newChange.setLeash(oldChange.getLeash());
+            }
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
index 1941d66..652a2ed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
@@ -70,8 +70,8 @@
     private int mMarginMenuStart;
     private int mMenuHeight;
     private int mMenuWidth;
-
     private final int mCaptionHeight;
+    private HandleMenuAnimator mHandleMenuAnimator;
 
 
     HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY,
@@ -111,20 +111,19 @@
         mHandleMenuWindow = mParentDecor.addWindow(
                 R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu",
                 t, ssg, x, y, mMenuWidth, mMenuHeight);
+        final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView();
+        mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight);
     }
 
     /**
      * Animates the appearance of the handle menu and its three pills.
      */
     private void animateHandleMenu() {
-        final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView();
-        final HandleMenuAnimator handleMenuAnimator = new HandleMenuAnimator(handleMenuView,
-                mMenuWidth, mCaptionHeight);
         if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
                 || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
-            handleMenuAnimator.animateCaptionHandleExpandToOpen();
+            mHandleMenuAnimator.animateCaptionHandleExpandToOpen();
         } else {
-            handleMenuAnimator.animateOpen();
+            mHandleMenuAnimator.animateOpen();
         }
     }
 
@@ -328,8 +327,16 @@
     }
 
     void close() {
-        mHandleMenuWindow.releaseView();
-        mHandleMenuWindow = null;
+        final Runnable after = () -> {
+            mHandleMenuWindow.releaseView();
+            mHandleMenuWindow = null;
+        };
+        if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+            mHandleMenuAnimator.animateCollapseIntoHandleClose(after);
+        } else {
+            mHandleMenuAnimator.animateClose(after);
+        }
     }
 
     static final class Builder {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 531de1f..8c5d4a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -26,6 +26,7 @@
 import android.view.View.TRANSLATION_Y
 import android.view.View.TRANSLATION_Z
 import android.view.ViewGroup
+import androidx.core.animation.doOnEnd
 import androidx.core.view.children
 import com.android.wm.shell.R
 import com.android.wm.shell.animation.Interpolators
@@ -37,27 +38,36 @@
     private val captionHeight: Float
 ) {
     companion object {
-        private const val MENU_Y_TRANSLATION_DURATION: Long = 150
-        private const val HEADER_NONFREEFORM_SCALE_DURATION: Long = 150
-        private const val HEADER_FREEFORM_SCALE_DURATION: Long = 217
-        private const val HEADER_ELEVATION_DURATION: Long = 83
-        private const val HEADER_CONTENT_ALPHA_DURATION: Long = 100
-        private const val BODY_SCALE_DURATION: Long = 180
-        private const val BODY_ALPHA_DURATION: Long = 150
-        private const val BODY_ELEVATION_DURATION: Long = 83
-        private const val BODY_CONTENT_ALPHA_DURATION: Long = 167
+        // Open animation constants
+        private const val MENU_Y_TRANSLATION_OPEN_DURATION: Long = 150
+        private const val HEADER_NONFREEFORM_SCALE_OPEN_DURATION: Long = 150
+        private const val HEADER_FREEFORM_SCALE_OPEN_DURATION: Long = 217
+        private const val HEADER_ELEVATION_OPEN_DURATION: Long = 83
+        private const val HEADER_CONTENT_ALPHA_OPEN_DURATION: Long = 100
+        private const val BODY_SCALE_OPEN_DURATION: Long = 180
+        private const val BODY_ALPHA_OPEN_DURATION: Long = 150
+        private const val BODY_ELEVATION_OPEN_DURATION: Long = 83
+        private const val BODY_CONTENT_ALPHA_OPEN_DURATION: Long = 167
 
-        private const val ELEVATION_DELAY: Long = 33
-        private const val HEADER_CONTENT_ALPHA_DELAY: Long = 67
-        private const val BODY_SCALE_DELAY: Long = 50
-        private const val BODY_ALPHA_DELAY: Long = 133
+        private const val ELEVATION_OPEN_DELAY: Long = 33
+        private const val HEADER_CONTENT_ALPHA_OPEN_DELAY: Long = 67
+        private const val BODY_SCALE_OPEN_DELAY: Long = 50
+        private const val BODY_ALPHA_OPEN_DELAY: Long = 133
 
         private const val HALF_INITIAL_SCALE: Float = 0.5f
         private const val NONFREEFORM_HEADER_INITIAL_SCALE_X: Float = 0.6f
         private const val NONFREEFORM_HEADER_INITIAL_SCALE_Y: Float = 0.05f
+
+        // Close animation constants
+        private const val HEADER_CLOSE_DELAY: Long = 20
+        private const val HEADER_CLOSE_DURATION: Long = 50
+        private const val HEADER_CONTENT_OPACITY_CLOSE_DELAY: Long = 25
+        private const val HEADER_CONTENT_OPACITY_CLOSE_DURATION: Long = 25
+        private const val BODY_CLOSE_DURATION: Long = 50
     }
 
     private val animators: MutableList<Animator> = mutableListOf()
+    private var runningAnimation: AnimatorSet? = null
 
     private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill)
     private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill)
@@ -67,9 +77,9 @@
     fun animateOpen() {
         prepareMenuForAnimation()
         appInfoPillExpand()
-        animateAppInfoPill()
-        animateWindowingPill()
-        animateMoreActionsPill()
+        animateAppInfoPillOpen()
+        animateWindowingPillOpen()
+        animateMoreActionsPillOpen()
         runAnimations()
     }
 
@@ -81,13 +91,44 @@
     fun animateCaptionHandleExpandToOpen() {
         prepareMenuForAnimation()
         captionHandleExpandIntoAppInfoPill()
-        animateAppInfoPill()
-        animateWindowingPill()
-        animateMoreActionsPill()
+        animateAppInfoPillOpen()
+        animateWindowingPillOpen()
+        animateMoreActionsPillOpen()
         runAnimations()
     }
 
     /**
+     * Animates the closing of the handle menu. The windowing and more actions pill vanish. Then,
+     * the app info pill will collapse into the shape of the caption handle in full screen and split
+     * screen.
+     *
+     * @param after runs after the animation finishes.
+     */
+    fun animateCollapseIntoHandleClose(after: Runnable) {
+        appInfoCollapseToHandle()
+        animateAppInfoPillFadeOut()
+        windowingPillClose()
+        moreActionsPillClose()
+        runAnimations(after)
+    }
+
+    /**
+     * Animates the closing of the handle menu. The windowing and more actions pill vanish. Then,
+     * the app info pill will collapse into the shape of the caption handle in full screen and split
+     * screen.
+     *
+     * @param after runs after animation finishes.
+     *
+     */
+    fun animateClose(after: Runnable) {
+        appInfoPillCollapse()
+        animateAppInfoPillFadeOut()
+        windowingPillClose()
+        moreActionsPillClose()
+        runAnimations(after)
+    }
+
+    /**
      * Prepares the handle menu for animation. Presets the opacity of necessary menu components.
      * Presets pivots of handle menu and body pills for scaling animation.
      */
@@ -108,20 +149,20 @@
         moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat()
     }
 
-    private fun animateAppInfoPill() {
+    private fun animateAppInfoPillOpen() {
         // Header Elevation Animation
         animators +=
             ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Z, 1f).apply {
-                startDelay = ELEVATION_DELAY
-                duration = HEADER_ELEVATION_DURATION
+                startDelay = ELEVATION_OPEN_DELAY
+                duration = HEADER_ELEVATION_OPEN_DURATION
             }
 
         // Content Opacity Animation
         appInfoPill.children.forEach {
             animators +=
                 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
-                    startDelay = HEADER_CONTENT_ALPHA_DELAY
-                    duration = HEADER_CONTENT_ALPHA_DURATION
+                    startDelay = HEADER_CONTENT_ALPHA_OPEN_DELAY
+                    duration = HEADER_CONTENT_ALPHA_OPEN_DURATION
                 }
         }
     }
@@ -130,17 +171,17 @@
         // Header scaling animation
         animators +=
             ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X, 1f)
-                .apply { duration = HEADER_NONFREEFORM_SCALE_DURATION }
+                .apply { duration = HEADER_NONFREEFORM_SCALE_OPEN_DURATION }
 
         animators +=
             ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y, 1f)
-                .apply { duration = HEADER_NONFREEFORM_SCALE_DURATION }
+                .apply { duration = HEADER_NONFREEFORM_SCALE_OPEN_DURATION }
 
         // Downward y-translation animation
         val yStart: Float = -captionHeight / 2
         animators +=
             ObjectAnimator.ofFloat(handleMenu, TRANSLATION_Y, yStart, 0f).apply {
-                duration = MENU_Y_TRANSLATION_DURATION
+                duration = MENU_Y_TRANSLATION_OPEN_DURATION
             }
     }
 
@@ -148,98 +189,217 @@
         // Header scaling animation
         animators +=
             ObjectAnimator.ofFloat(appInfoPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
-                duration = HEADER_FREEFORM_SCALE_DURATION
+                duration = HEADER_FREEFORM_SCALE_OPEN_DURATION
             }
 
         animators +=
             ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
-                duration = HEADER_FREEFORM_SCALE_DURATION
+                duration = HEADER_FREEFORM_SCALE_OPEN_DURATION
             }
     }
 
-    private fun animateWindowingPill() {
+    private fun animateWindowingPillOpen() {
         // Windowing X & Y Scaling Animation
         animators +=
             ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
-                startDelay = BODY_SCALE_DELAY
-                duration = BODY_SCALE_DURATION
+                startDelay = BODY_SCALE_OPEN_DELAY
+                duration = BODY_SCALE_OPEN_DURATION
             }
 
         animators +=
             ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
-                startDelay = BODY_SCALE_DELAY
-                duration = BODY_SCALE_DURATION
+                startDelay = BODY_SCALE_OPEN_DELAY
+                duration = BODY_SCALE_OPEN_DURATION
             }
 
         // Windowing Opacity Animation
         animators +=
             ObjectAnimator.ofFloat(windowingPill, ALPHA, 1f).apply {
-                startDelay = BODY_ALPHA_DELAY
-                duration = BODY_ALPHA_DURATION
+                startDelay = BODY_ALPHA_OPEN_DELAY
+                duration = BODY_ALPHA_OPEN_DURATION
             }
 
         // Windowing Elevation Animation
         animators +=
             ObjectAnimator.ofFloat(windowingPill, TRANSLATION_Z, 1f).apply {
-                startDelay = ELEVATION_DELAY
-                duration = BODY_ELEVATION_DURATION
+                startDelay = ELEVATION_OPEN_DELAY
+                duration = BODY_ELEVATION_OPEN_DURATION
             }
 
         // Windowing Content Opacity Animation
         windowingPill.children.forEach {
             animators +=
                 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
-                    startDelay = BODY_ALPHA_DELAY
-                    duration = BODY_CONTENT_ALPHA_DURATION
+                    startDelay = BODY_ALPHA_OPEN_DELAY
+                    duration = BODY_CONTENT_ALPHA_OPEN_DURATION
                     interpolator = Interpolators.FAST_OUT_SLOW_IN
                 }
         }
     }
 
-    private fun animateMoreActionsPill() {
+    private fun animateMoreActionsPillOpen() {
         // More Actions X & Y Scaling Animation
         animators +=
             ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
-                startDelay = BODY_SCALE_DELAY
-                duration = BODY_SCALE_DURATION
+                startDelay = BODY_SCALE_OPEN_DELAY
+                duration = BODY_SCALE_OPEN_DURATION
             }
 
         animators +=
             ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
-                startDelay = BODY_SCALE_DELAY
-                duration = BODY_SCALE_DURATION
+                startDelay = BODY_SCALE_OPEN_DELAY
+                duration = BODY_SCALE_OPEN_DURATION
             }
 
         // More Actions Opacity Animation
         animators +=
             ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 1f).apply {
-                startDelay = BODY_ALPHA_DELAY
-                duration = BODY_ALPHA_DURATION
+                startDelay = BODY_ALPHA_OPEN_DELAY
+                duration = BODY_ALPHA_OPEN_DURATION
             }
 
         // More Actions Elevation Animation
         animators +=
             ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Z, 1f).apply {
-                startDelay = ELEVATION_DELAY
-                duration = BODY_ELEVATION_DURATION
+                startDelay = ELEVATION_OPEN_DELAY
+                duration = BODY_ELEVATION_OPEN_DURATION
             }
 
         // More Actions Content Opacity Animation
         moreActionsPill.children.forEach {
             animators +=
                 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
-                    startDelay = BODY_ALPHA_DELAY
-                    duration = BODY_CONTENT_ALPHA_DURATION
+                    startDelay = BODY_ALPHA_OPEN_DELAY
+                    duration = BODY_CONTENT_ALPHA_OPEN_DURATION
                     interpolator = Interpolators.FAST_OUT_SLOW_IN
                 }
         }
     }
 
-    /** Runs the list of animators concurrently. */
-    private fun runAnimations() {
-        val animatorSet = AnimatorSet()
-        animatorSet.playTogether(animators)
-        animatorSet.start()
-        animators.clear()
+    private fun appInfoPillCollapse() {
+        // Header scaling animation
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_X, 0f).apply {
+                startDelay = HEADER_CLOSE_DELAY
+                duration = HEADER_CLOSE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, 0f).apply {
+                startDelay = HEADER_CLOSE_DELAY
+                duration = HEADER_CLOSE_DURATION
+            }
+    }
+
+    private fun appInfoCollapseToHandle() {
+        // Header X & Y Scaling Animation
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X).apply {
+                startDelay = HEADER_CLOSE_DELAY
+                duration = HEADER_CLOSE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y).apply {
+                startDelay = HEADER_CLOSE_DELAY
+                duration = HEADER_CLOSE_DURATION
+            }
+        // Upward y-translation animation
+        val yStart: Float = -captionHeight / 2
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Y, yStart).apply {
+                startDelay = HEADER_CLOSE_DELAY
+                duration = HEADER_CLOSE_DURATION
+            }
+    }
+
+    private fun animateAppInfoPillFadeOut() {
+        // Header Content Opacity Animation
+        appInfoPill.children.forEach {
+            animators +=
+                ObjectAnimator.ofFloat(it, ALPHA, 0f).apply {
+                    startDelay = HEADER_CONTENT_OPACITY_CLOSE_DELAY
+                    duration = HEADER_CONTENT_OPACITY_CLOSE_DURATION
+                }
+        }
+    }
+
+    private fun windowingPillClose() {
+        // Windowing X & Y Scaling Animation
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        // windowing Animation
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, ALPHA, 0f).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, ALPHA, 0f).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+    }
+
+    private fun moreActionsPillClose() {
+        // More Actions X & Y Scaling Animation
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        // More Actions Opacity Animation
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 0f).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 0f).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+
+        // upward more actions pill y-translation animation
+        val yStart: Float = -captionHeight / 2
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Y, yStart).apply {
+                duration = BODY_CLOSE_DURATION
+            }
+    }
+
+    /**
+     * Runs the list of hide animators concurrently.
+     *
+     * @param after runs after animation finishes.
+     */
+    private fun runAnimations(after: Runnable? = null) {
+        runningAnimation?.apply {
+            // Remove all listeners, so that after runnable isn't triggered upon cancel.
+            removeAllListeners()
+            // If an animation runs while running animation is triggered, gracefully cancel.
+            cancel()
+        }
+
+        runningAnimation = AnimatorSet().apply {
+            playTogether(animators)
+            animators.clear()
+            doOnEnd {
+                after?.run()
+                runningAnimation = null
+            }
+            start()
+        }
     }
 }
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index d28bb49..3e131bc 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -40,6 +40,7 @@
 #ifdef __ANDROID__
 #include "include/gpu/ganesh/SkImageGanesh.h"
 #endif
+#include "utils/ForceDark.h"
 #include "utils/MathUtils.h"
 #include "utils/StringUtils.h"
 
@@ -403,16 +404,21 @@
     deleteDisplayList(observer, info);
     mDisplayList = std::move(mStagingDisplayList);
     if (mDisplayList) {
-        WebViewSyncData syncData {
-            .applyForceDark = info && !info->disableForceDark
-        };
+        WebViewSyncData syncData{.applyForceDark = shouldEnableForceDark(info)};
         mDisplayList.syncContents(syncData);
         handleForceDark(info);
     }
 }
 
+inline bool RenderNode::shouldEnableForceDark(TreeInfo* info) {
+    return CC_UNLIKELY(
+            info &&
+            (!info->disableForceDark ||
+             info->forceDarkType == android::uirenderer::ForceDarkType::FORCE_INVERT_COLOR_DARK));
+}
+
 void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
-    if (CC_LIKELY(!info || info->disableForceDark)) {
+    if (!shouldEnableForceDark(info)) {
         return;
     }
     auto usage = usageHint();
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index c959db3..1f3834be 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -233,6 +233,7 @@
     void syncProperties();
     void syncDisplayList(TreeObserver& observer, TreeInfo* info);
     void handleForceDark(TreeInfo* info);
+    bool shouldEnableForceDark(TreeInfo* info);
 
     void prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer);
     void pushStagingPropertiesChanges(TreeInfo& info);
diff --git a/libs/hwui/TreeInfo.cpp b/libs/hwui/TreeInfo.cpp
index 750f869..717157c 100644
--- a/libs/hwui/TreeInfo.cpp
+++ b/libs/hwui/TreeInfo.cpp
@@ -24,7 +24,8 @@
         : mode(mode)
         , prepareTextures(mode == MODE_FULL)
         , canvasContext(canvasContext)
-        , disableForceDark(canvasContext.useForceDark() ? 0 : 1)
+        , disableForceDark(canvasContext.getForceDarkType() == ForceDarkType::NONE ? 1 : 0)
+        , forceDarkType(canvasContext.getForceDarkType())
         , screenSize(canvasContext.getNextFrameSize()) {}
 
 }  // namespace android::uirenderer
diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h
index ea25f68..88449f3 100644
--- a/libs/hwui/TreeInfo.h
+++ b/libs/hwui/TreeInfo.h
@@ -24,6 +24,7 @@
 #include "Properties.h"
 #include "SkSize.h"
 #include "SkippedFrameInfo.h"
+#include "utils/ForceDark.h"
 #include "utils/Macros.h"
 
 namespace android {
@@ -97,6 +98,7 @@
     bool updateWindowPositions = false;
 
     int disableForceDark;
+    ForceDarkType forceDarkType = ForceDarkType::NONE;
 
     const SkISize screenSize;
 
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 422ffea..d15b1680 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -35,6 +35,7 @@
 #include <gui/TraceUtils.h>
 #include <include/encode/SkPngEncoder.h>
 #include <inttypes.h>
+#include <log/log.h>
 #include <media/NdkImage.h>
 #include <media/NdkImageReader.h>
 #include <nativehelper/JNIPlatformHelp.h>
@@ -53,11 +54,11 @@
 
 #include <algorithm>
 #include <atomic>
-#include <log/log.h>
 #include <vector>
 
 #include "JvmErrorReporter.h"
 #include "android_graphics_HardwareRendererObserver.h"
+#include "utils/ForceDark.h"
 
 namespace android {
 
@@ -824,10 +825,10 @@
     proxy->allocateBuffers();
 }
 
-static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz,
-        jlong proxyPtr, jboolean enable) {
+static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz, jlong proxyPtr,
+                                                       jint type) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    proxy->setForceDark(enable);
+    proxy->setForceDark(static_cast<ForceDarkType>(type));
 }
 
 static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) {
@@ -1016,7 +1017,7 @@
         {"nSetIsolatedProcess", "(Z)V", (void*)android_view_ThreadedRenderer_setIsolatedProcess},
         {"nSetContextPriority", "(I)V", (void*)android_view_ThreadedRenderer_setContextPriority},
         {"nAllocateBuffers", "(J)V", (void*)android_view_ThreadedRenderer_allocateBuffers},
-        {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark},
+        {"nSetForceDark", "(JI)V", (void*)android_view_ThreadedRenderer_setForceDark},
         {"nSetDisplayDensityDpi", "(I)V",
          (void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
         {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 37e4f7ec..be9b649 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -46,6 +46,7 @@
 #include "renderstate/RenderState.h"
 #include "renderthread/RenderTask.h"
 #include "renderthread/RenderThread.h"
+#include "utils/ForceDark.h"
 #include "utils/RingBuffer.h"
 
 namespace android {
@@ -194,11 +195,9 @@
         mRenderPipeline->setPictureCapturedCallback(callback);
     }
 
-    void setForceDark(bool enable) { mUseForceDark = enable; }
+    void setForceDark(ForceDarkType type) { mForceDarkType = type; }
 
-    bool useForceDark() {
-        return mUseForceDark;
-    }
+    ForceDarkType getForceDarkType() { return mForceDarkType; }
 
     SkISize getNextFrameSize() const;
 
@@ -321,7 +320,7 @@
     nsecs_t mLastDropVsync = 0;
 
     bool mOpaque;
-    bool mUseForceDark = false;
+    ForceDarkType mForceDarkType = ForceDarkType::NONE;
     LightInfo mLightInfo;
     LightGeometry mLightGeometry = {{0, 0, 0}, 0};
 
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index be163ba..c3c136f 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -417,8 +417,8 @@
     });
 }
 
-void RenderProxy::setForceDark(bool enable) {
-    mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
+void RenderProxy::setForceDark(ForceDarkType type) {
+    mRenderThread.queue().post([this, type]() { mContext->setForceDark(type); });
 }
 
 void RenderProxy::copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request) {
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 47c1b0c..f2d8e94 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -31,6 +31,7 @@
 #include "DrawFrameTask.h"
 #include "SwapBehavior.h"
 #include "hwui/Bitmap.h"
+#include "utils/ForceDark.h"
 
 class SkBitmap;
 class SkPicture;
@@ -142,7 +143,7 @@
 
     void addFrameMetricsObserver(FrameMetricsObserver* observer);
     void removeFrameMetricsObserver(FrameMetricsObserver* observer);
-    void setForceDark(bool enable);
+    void setForceDark(ForceDarkType type);
 
     static void copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request);
     static void prepareToDraw(Bitmap& bitmap);
diff --git a/libs/hwui/utils/ForceDark.h b/libs/hwui/utils/ForceDark.h
new file mode 100644
index 0000000..28538c4b
--- /dev/null
+++ b/libs/hwui/utils/ForceDark.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FORCEDARKUTILS_H
+#define FORCEDARKUTILS_H
+
+namespace android {
+namespace uirenderer {
+
+/**
+ * The type of force dark set on the renderer, if any.
+ *
+ * This should stay in sync with the java @IntDef in
+ * frameworks/base/graphics/java/android/graphics/ForceDarkType.java
+ */
+enum class ForceDarkType : __uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
+
+} /* namespace uirenderer */
+} /* namespace android */
+
+#endif  // FORCEDARKUTILS_H
\ No newline at end of file
diff --git a/location/api/lint-baseline.txt b/location/api/lint-baseline.txt
new file mode 100644
index 0000000..5e3ef01
--- /dev/null
+++ b/location/api/lint-baseline.txt
@@ -0,0 +1,27 @@
+// Baseline format: 1.0
+RequiresPermission: android.location.LocationManager#addGpsStatusListener(android.location.GpsStatus.Listener):
+    Method 'addGpsStatusListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener, android.os.Handler):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(java.util.concurrent.Executor, android.location.OnNmeaMessageListener):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addProximityAlert(double, double, float, long, android.app.PendingIntent):
+    Method 'addProximityAlert' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssMeasurementRequest, java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssMeasurementsEvent.Callback, android.os.Handler):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssNavigationMessageCallback(android.location.GnssNavigationMessage.Callback, android.os.Handler):
+    Method 'registerGnssNavigationMessageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssNavigationMessageCallback(java.util.concurrent.Executor, android.location.GnssNavigationMessage.Callback):
+    Method 'registerGnssNavigationMessageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(android.location.GnssStatus.Callback):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(android.location.GnssStatus.Callback, android.os.Handler):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(java.util.concurrent.Executor, android.location.GnssStatus.Callback):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
diff --git a/location/api/module-lib-lint-baseline.txt b/location/api/module-lib-lint-baseline.txt
index 7cd6a86..3b1be7db 100644
--- a/location/api/module-lib-lint-baseline.txt
+++ b/location/api/module-lib-lint-baseline.txt
@@ -1,4 +1,36 @@
 // Baseline format: 1.0
+RequiresPermission: android.location.LocationManager#addGpsStatusListener(android.location.GpsStatus.Listener):
+    Method 'addGpsStatusListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener, android.os.Handler):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(java.util.concurrent.Executor, android.location.OnNmeaMessageListener):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addProximityAlert(double, double, float, long, android.app.PendingIntent):
+    Method 'addProximityAlert' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#injectGnssMeasurementCorrections(android.location.GnssMeasurementCorrections):
+    Method 'injectGnssMeasurementCorrections' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssMeasurementRequest, java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssMeasurementsEvent.Callback, android.os.Handler):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssRequest, java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssNavigationMessageCallback(android.location.GnssNavigationMessage.Callback, android.os.Handler):
+    Method 'registerGnssNavigationMessageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssNavigationMessageCallback(java.util.concurrent.Executor, android.location.GnssNavigationMessage.Callback):
+    Method 'registerGnssNavigationMessageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(android.location.GnssStatus.Callback):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(android.location.GnssStatus.Callback, android.os.Handler):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(java.util.concurrent.Executor, android.location.GnssStatus.Callback):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+
+
 SamShouldBeLast: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener, android.os.Handler):
     SAM-compatible parameters (such as parameter 1, "listener", in android.location.LocationManager.addNmeaListener) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
 SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener, android.os.Looper):
diff --git a/location/api/system-lint-baseline.txt b/location/api/system-lint-baseline.txt
index 043a082..066a40a 100644
--- a/location/api/system-lint-baseline.txt
+++ b/location/api/system-lint-baseline.txt
@@ -1,4 +1,36 @@
 // Baseline format: 1.0
+RequiresPermission: android.location.LocationManager#addGpsStatusListener(android.location.GpsStatus.Listener):
+    Method 'addGpsStatusListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener, android.os.Handler):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addNmeaListener(java.util.concurrent.Executor, android.location.OnNmeaMessageListener):
+    Method 'addNmeaListener' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#addProximityAlert(double, double, float, long, android.app.PendingIntent):
+    Method 'addProximityAlert' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#injectGnssMeasurementCorrections(android.location.GnssMeasurementCorrections):
+    Method 'injectGnssMeasurementCorrections' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssMeasurementRequest, java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssMeasurementsEvent.Callback, android.os.Handler):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(android.location.GnssRequest, java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssMeasurementsCallback(java.util.concurrent.Executor, android.location.GnssMeasurementsEvent.Callback):
+    Method 'registerGnssMeasurementsCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssNavigationMessageCallback(android.location.GnssNavigationMessage.Callback, android.os.Handler):
+    Method 'registerGnssNavigationMessageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssNavigationMessageCallback(java.util.concurrent.Executor, android.location.GnssNavigationMessage.Callback):
+    Method 'registerGnssNavigationMessageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(android.location.GnssStatus.Callback):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(android.location.GnssStatus.Callback, android.os.Handler):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.location.LocationManager#registerGnssStatusCallback(java.util.concurrent.Executor, android.location.GnssStatus.Callback):
+    Method 'registerGnssStatusCallback' documentation mentions permissions already declared by @RequiresPermission
+
+
 SamShouldBeLast: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener, android.os.Handler):
     SAM-compatible parameters (such as parameter 1, "listener", in android.location.LocationManager.addNmeaListener) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
 SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener, android.os.Looper):
diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp
index 48cd8b6..c9a8864 100644
--- a/media/tests/projection/Android.bp
+++ b/media/tests/projection/Android.bp
@@ -26,6 +26,7 @@
         "androidx.test.runner",
         "androidx.test.rules",
         "androidx.test.ext.junit",
+        "frameworks-base-testutils",
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "testng",
diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
index 4952e01..774de5f 100644
--- a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
+++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
@@ -16,7 +16,11 @@
 
 package android.media.projection;
 
+import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
+
+import android.annotation.EnforcePermission;
 import android.os.IBinder;
+import android.os.PermissionEnforcer;
 import android.os.RemoteException;
 
 /**
@@ -28,6 +32,10 @@
     IBinder mLaunchCookie = null;
     IMediaProjectionCallback mIMediaProjectionCallback = null;
 
+    FakeIMediaProjection(PermissionEnforcer enforcer) {
+        super(enforcer);
+    }
+
     @Override
     public void start(IMediaProjectionCallback callback) throws RemoteException {
         mIMediaProjectionCallback = callback;
@@ -56,7 +64,9 @@
     }
 
     @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public int applyVirtualDisplayFlags(int flags) throws RemoteException {
+        applyVirtualDisplayFlags_enforcePermission();
         return 0;
     }
 
@@ -69,22 +79,30 @@
     }
 
     @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public IBinder getLaunchCookie() throws RemoteException {
+        getLaunchCookie_enforcePermission();
         return mLaunchCookie;
     }
 
     @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public void setLaunchCookie(IBinder launchCookie) throws RemoteException {
+        setLaunchCookie_enforcePermission();
         mLaunchCookie = launchCookie;
     }
 
     @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public boolean isValid() throws RemoteException {
+        isValid_enforcePermission();
         return true;
     }
 
 
     @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public void notifyVirtualDisplayCreated(int displayId) throws RemoteException {
+        notifyVirtualDisplayCreated_enforcePermission();
     }
 }
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
index 2a5674e..2e0396f 100644
--- a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
@@ -16,7 +16,7 @@
 
 package android.media.projection;
 
-
+import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
 import static android.media.projection.MediaProjection.MEDIA_PROJECTION_REQUIRES_CALLBACK;
 import static android.view.Display.DEFAULT_DISPLAY;
 
@@ -42,6 +42,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.test.FakePermissionEnforcer;
 import android.platform.test.annotations.Presubmit;
 import android.testing.TestableContext;
 import android.view.Display;
@@ -80,7 +81,7 @@
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     // Fake the connection to the system server.
-    private final FakeIMediaProjection mFakeIMediaProjection = new FakeIMediaProjection();
+    private FakeIMediaProjection mFakeIMediaProjection;
     // Callback registered by an app.
     private MediaProjection mMediaProjection;
 
@@ -112,7 +113,10 @@
                         .strictness(Strictness.LENIENT)
                         .startMocking();
 
+        FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
+        permissionEnforcer.grant(MANAGE_MEDIA_PROJECTION);
         // Support the MediaProjection instance.
+        mFakeIMediaProjection = new FakeIMediaProjection(permissionEnforcer);
         mFakeIMediaProjection.setLaunchCookie(mock(IBinder.class));
         mMediaProjection = new MediaProjection(mTestableContext, mFakeIMediaProjection,
                 mDisplayManager);
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index 6001155..2b5fcd8 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -24,8 +24,8 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
 import android.widget.LinearLayout;
-import android.widget.Switch;
 import android.widget.TextView;
 
 import androidx.annotation.ColorInt;
@@ -41,9 +41,9 @@
  * This component is used as the main switch of the page
  * to enable or disable the prefereces on the page.
  */
-public class MainSwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener {
+public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListener {
 
-    private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+    private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
 
     @ColorInt
     private int mBackgroundColor;
@@ -51,8 +51,8 @@
     private int mBackgroundActivatedColor;
 
     protected TextView mTextView;
-    protected Switch mSwitch;
-    private View mFrameView;
+    protected CompoundButton mSwitch;
+    private final View mFrameView;
 
     public MainSwitchBar(Context context) {
         this(context, null);
@@ -84,8 +84,8 @@
         setClickable(true);
 
         mFrameView = findViewById(R.id.frame);
-        mTextView = (TextView) findViewById(R.id.switch_text);
-        mSwitch = (Switch) findViewById(android.R.id.switch_widget);
+        mTextView = findViewById(R.id.switch_text);
+        mSwitch = findViewById(android.R.id.switch_widget);
         addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked));
 
         if (mSwitch.getVisibility() == VISIBLE) {
@@ -136,13 +136,6 @@
     }
 
     /**
-     * Return the Switch
-     */
-    public final Switch getSwitch() {
-        return mSwitch;
-    }
-
-    /**
      * Set the title text
      */
     public void setTitle(CharSequence text) {
@@ -192,7 +185,7 @@
     /**
      * Adds a listener for switch changes
      */
-    public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
         if (!mSwitchChangeListeners.contains(listener)) {
             mSwitchChangeListeners.add(listener);
         }
@@ -201,7 +194,7 @@
     /**
      * Remove a listener for switch changes
      */
-    public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
         mSwitchChangeListeners.remove(listener);
     }
 
@@ -223,9 +216,8 @@
     private void propagateChecked(boolean isChecked) {
         setBackground(isChecked);
 
-        final int count = mSwitchChangeListeners.size();
-        for (int n = 0; n < count; n++) {
-            mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked);
+        for (OnCheckedChangeListener changeListener : mSwitchChangeListeners) {
+            changeListener.onCheckedChanged(mSwitch, isChecked);
         }
     }
 
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index 11a6804..b294d4e 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -19,24 +19,25 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.util.AttributeSet;
-import android.widget.Switch;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
 
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.widget.mainswitch.R;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import com.android.settingslib.widget.mainswitch.R;
-
 /**
  * MainSwitchPreference is a Preference with a customized Switch.
  * This component is used as the main switch of the page
  * to enable or disable the prefereces on the page.
  */
-public class MainSwitchPreference extends TwoStatePreference implements OnMainSwitchChangeListener {
+public class MainSwitchPreference extends TwoStatePreference implements OnCheckedChangeListener {
 
-    private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+    private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
 
     private MainSwitchBar mMainSwitchBar;
 
@@ -120,7 +121,7 @@
     }
 
     @Override
-    public void onSwitchChanged(Switch switchView, boolean isChecked) {
+    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
         super.setChecked(isChecked);
     }
 
@@ -138,7 +139,7 @@
     /**
      * Adds a listener for switch changes
      */
-    public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
         if (!mSwitchChangeListeners.contains(listener)) {
             mSwitchChangeListeners.add(listener);
         }
@@ -151,7 +152,7 @@
     /**
      * Remove a listener for switch changes
      */
-    public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
         mSwitchChangeListeners.remove(listener);
         if (mMainSwitchBar != null) {
             mMainSwitchBar.removeOnSwitchChangeListener(listener);
@@ -159,7 +160,7 @@
     }
 
     private void registerListenerToSwitchBar() {
-        for (OnMainSwitchChangeListener listener : mSwitchChangeListeners) {
+        for (OnCheckedChangeListener listener : mSwitchChangeListeners) {
             mMainSwitchBar.addOnSwitchChangeListener(listener);
         }
     }
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java
deleted file mode 100644
index 03868f9..0000000
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.widget;
-
-import android.widget.Switch;
-
-import com.android.settingslib.widget.mainswitch.R;
-
-/**
- * Called when the checked state of the Switch has changed.
- */
-public interface OnMainSwitchChangeListener {
-    /**
-     * @param switchView The Switch view whose state has changed.
-     * @param isChecked  The new checked state of switchView.
-     */
-    void onSwitchChanged(Switch switchView, boolean isChecked);
-}
diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
index ec60f8c..18a6db0 100644
--- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml
+++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
@@ -26,4 +26,9 @@
     <string name="single_line_summary_preference_summary" translatable="false">A very long summary to show case a preference which only shows a single line summary.</string>
     <!-- Footer text with two links. [DO NOT TRANSLATE] -->
     <string name="footer_with_two_links" translatable="false">Annotated string with <a href="https://www.android.com/">link 1</a> and <a href="https://source.android.com/">link 2</a>.</string>
+
+    <!-- Sample title -->
+    <string name="sample_title" translatable="false">Lorem ipsum</string>
+    <!-- Sample text -->
+    <string name="sample_text" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent a rhoncus tellus. Nulla facilisi. Pellentesque erat ex, maximus viae turpis</string>
 </resources>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index d62b490..b1e1585 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironment
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
+import com.android.settingslib.spa.gallery.card.CardPageProvider
 import com.android.settingslib.spa.gallery.chart.ChartPageProvider
 import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
@@ -98,6 +99,7 @@
                 SettingsExposedDropdownMenuCheckBoxProvider,
                 SettingsTextFieldPasswordPageProvider,
                 SearchScaffoldPageProvider,
+                CardPageProvider,
             ),
             rootPages = listOf(
                 HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
new file mode 100644
index 0000000..e914d5c
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.card
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.WarningAmber
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.gallery.R
+import com.android.settingslib.spa.widget.card.CardButton
+import com.android.settingslib.spa.widget.card.SettingsCard
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+object CardPageProvider : SettingsPageProvider {
+    override val name = "ActionButton"
+
+    override fun getTitle(arguments: Bundle?) = TITLE
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(title = TITLE) {
+            SettingsCardWithIcon()
+            SettingsCardWithoutIcon()
+        }
+    }
+
+    @Composable
+    private fun SettingsCardWithIcon() {
+        SettingsCard(
+            title = stringResource(R.string.sample_title),
+            text = stringResource(R.string.sample_text),
+            imageVector = Icons.Outlined.WarningAmber,
+            buttons = listOf(
+                CardButton(text = "Action") {},
+                CardButton(text = "Action", isMain = true) {},
+            )
+        )
+    }
+
+    @Composable
+    private fun SettingsCardWithoutIcon() {
+        SettingsCard(
+            title = stringResource(R.string.sample_title),
+            text = stringResource(R.string.sample_text),
+            buttons = listOf(
+                CardButton(text = "Action") {},
+            )
+        )
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    private const val TITLE = "Sample Card"
+}
+
+@Preview
+@Composable
+private fun CardPagePreview() {
+    SettingsTheme {
+        CardPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index b339b44..f52ceec 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -28,6 +28,7 @@
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
+import com.android.settingslib.spa.gallery.card.CardPageProvider
 import com.android.settingslib.spa.gallery.chart.ChartPageProvider
 import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
@@ -69,6 +70,7 @@
             ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
         )
     }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt
index a1ab35b..eac06e3 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt
@@ -50,7 +50,7 @@
 
 @Composable
 private fun Page() {
-    SearchScaffold(title = TITLE) { bottomPadding, searchQuery ->
-        PlaceholderTitle("Search query: ${searchQuery.value}")
+    SearchScaffold(title = TITLE) { _, searchQuery ->
+        PlaceholderTitle("Search query: ${searchQuery()}")
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt
index ba88546..b97fb9c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt
@@ -29,12 +29,6 @@
 }
 
 /**
- * Remember the [State] initialized with the [this].
- */
-@Composable
-fun <T> T.toState(): State<T> = remember { stateOf(this) }
-
-/**
  * Return a new [State] initialized with the passed in [value].
  */
 fun <T> stateOf(value: T) = object : State<T> {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/StateFlowBridge.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/StateFlowBridge.kt
index 494e69b..7842948 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/StateFlowBridge.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/StateFlowBridge.kt
@@ -18,11 +18,10 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 
-/** A StateFlow holder which value could be set or sync from [State]. */
+/** A StateFlow holder which value could be set or sync from callback. */
 class StateFlowBridge<T> {
     private val stateFlow = MutableStateFlow<T?>(null)
     val flow = stateFlow.filterNotNull()
@@ -34,9 +33,10 @@
     }
 
     @Composable
-    fun Sync(state: State<T>) {
-        LaunchedEffect(state.value) {
-            stateFlow.value = state.value
+    fun Sync(callback: () -> T) {
+        val value = callback()
+        LaunchedEffect(value) {
+            stateFlow.value = value
         }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
new file mode 100644
index 0000000..98873e0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.card
+
+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.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.WarningAmber
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraLarge
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.ui.SettingsBody
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+data class CardButton(
+    val text: String,
+    val isMain: Boolean = false,
+    val onClick: () -> Unit,
+)
+
+@Composable
+fun SettingsCard(
+    title: String,
+    text: String,
+    imageVector: ImageVector? = null,
+    buttons: List<CardButton> = emptyList(),
+) {
+    Card(
+        shape = CornerExtraLarge,
+        colors = CardDefaults.cardColors(
+            containerColor = SettingsTheme.colorScheme.surface,
+        ),
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(
+                horizontal = SettingsDimension.itemPaddingEnd,
+                vertical = SettingsDimension.itemPaddingAround,
+            ),
+    ) {
+        Column(
+            modifier = Modifier.padding(SettingsDimension.itemPaddingStart),
+            verticalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround)
+        ) {
+            CardIcon(imageVector)
+            SettingsTitle(title)
+            SettingsBody(text)
+            Buttons(buttons)
+        }
+    }
+}
+
+@Composable
+private fun CardIcon(imageVector: ImageVector?) {
+    if (imageVector != null) {
+        Icon(
+            imageVector = imageVector,
+            contentDescription = null,
+            modifier = Modifier.size(SettingsDimension.itemIconSize),
+            tint = MaterialTheme.colorScheme.primary,
+        )
+    }
+}
+
+@Composable
+private fun Buttons(buttons: List<CardButton>) {
+    if (buttons.isNotEmpty()) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(top = SettingsDimension.itemPaddingAround),
+            horizontalArrangement = Arrangement.spacedBy(
+                space = SettingsDimension.itemPaddingEnd,
+                alignment = Alignment.End,
+            ),
+        ) {
+            for (button in buttons) {
+                Button(button)
+            }
+        }
+    }
+}
+
+@Composable
+private fun Button(button: CardButton) {
+    if (button.isMain) {
+        Button(
+            onClick = button.onClick,
+            colors = ButtonDefaults.buttonColors(
+                containerColor = SettingsTheme.colorScheme.primaryContainer,
+            ),
+        ) {
+            Text(
+                text = button.text,
+                color = SettingsTheme.colorScheme.onPrimaryContainer,
+            )
+        }
+    } else {
+        OutlinedButton(onClick = button.onClick) {
+            Text(
+                text = button.text,
+                color = MaterialTheme.colorScheme.onSurface,
+            )
+        }
+    }
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Composable
+private fun SettingsCardPreviewLight() {
+    SettingsCardPreview()
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun SettingsCardPreviewDark() {
+    SettingsCardPreview()
+}
+
+@Composable
+private fun SettingsCardPreview() {
+    SettingsTheme {
+        SettingsCard(
+            title = "Lorem ipsum",
+            text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+            imageVector = Icons.Outlined.WarningAmber,
+            buttons = listOf(
+                CardButton(text = "Action") {},
+                CardButton(text = "Action", isMain = true) {},
+            )
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
index 696e877..c87178d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -37,8 +37,6 @@
 import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -72,7 +70,7 @@
 fun SearchScaffold(
     title: String,
     actions: @Composable RowScope.() -> Unit = {},
-    content: @Composable (bottomPadding: Dp, searchQuery: State<String>) -> Unit,
+    content: @Composable (bottomPadding: Dp, searchQuery: () -> String) -> Unit,
 ) {
     ActivityTitle(title)
     var isSearchMode by rememberSaveable { mutableStateOf(false) }
@@ -100,12 +98,9 @@
                 .focusable()
                 .fillMaxSize()
         ) {
-            content(
-                paddingValues.calculateBottomPadding(),
-                remember {
-                    derivedStateOf { if (isSearchMode) viewModel.searchQuery.text else "" }
-                },
-            )
+            content(paddingValues.calculateBottomPadding()) {
+                if (isSearchMode) viewModel.searchQuery.text else ""
+            }
         }
     }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/StateFlowBridgeTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/StateFlowBridgeTest.kt
index f0e57b9..9b7ef08 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/StateFlowBridgeTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/StateFlowBridgeTest.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.compose.stateOf
 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -56,7 +55,7 @@
         val stateFlowBridge = StateFlowBridge<String>()
 
         composeTestRule.setContent {
-            stateFlowBridge.Sync(stateOf("A"))
+            stateFlowBridge.Sync { "A" }
         }
 
         val first = stateFlowBridge.flow.firstWithTimeoutOrNull()
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
new file mode 100644
index 0000000..0ec8507
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.card
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsCardTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun settingsCard_titleDisplayed() {
+        composeTestRule.setContent {
+            SettingsCard(
+                title = TITLE,
+                text = "",
+            )
+        }
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsCard_textDisplayed() {
+        composeTestRule.setContent {
+            SettingsCard(
+                title = "",
+                text = TEXT,
+            )
+        }
+
+        composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsCard_buttonDisplayed() {
+        composeTestRule.setContent {
+            SettingsCard(
+                title = "",
+                text = "",
+                buttons = listOf(
+                    CardButton(text = TEXT) {}
+                ),
+            )
+        }
+
+        composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsCard_buttonCanBeClicked() {
+        var buttonClicked = false
+        composeTestRule.setContent {
+            SettingsCard(
+                title = "",
+                text = "",
+                buttons = listOf(
+                    CardButton(text = TEXT) { buttonClicked = true }
+                ),
+            )
+        }
+
+        composeTestRule.onNodeWithText(TEXT).performClick()
+
+        assertThat(buttonClicked).isTrue()
+    }
+
+    private companion object {
+        const val TITLE = "Title"
+        const val TEXT = "Text"
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
index c3e1d54..826a0d4 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import androidx.appcompat.R
 import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.State
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithContentDescription
@@ -60,7 +59,7 @@
     fun initialState_searchQueryIsEmpty() {
         val searchQuery = setContent()
 
-        assertThat(searchQuery.value).isEqualTo("")
+        assertThat(searchQuery()).isEqualTo("")
     }
 
     @Test
@@ -72,7 +71,7 @@
         composeTestRule.onNodeWithText(TITLE).assertDoesNotExist()
         onSearchHint().assertIsDisplayed()
         onClearButton().assertDoesNotExist()
-        assertThat(searchQuery.value).isEqualTo("")
+        assertThat(searchQuery()).isEqualTo("")
     }
 
     @Test
@@ -87,7 +86,7 @@
         composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
         onSearchHint().assertDoesNotExist()
         onClearButton().assertDoesNotExist()
-        assertThat(searchQuery.value).isEqualTo("")
+        assertThat(searchQuery()).isEqualTo("")
     }
 
     @Test
@@ -98,7 +97,7 @@
         onSearchHint().performTextInput(QUERY)
 
         onClearButton().assertIsDisplayed()
-        assertThat(searchQuery.value).isEqualTo(QUERY)
+        assertThat(searchQuery()).isEqualTo(QUERY)
     }
 
     @Test
@@ -110,11 +109,11 @@
         onClearButton().performClick()
 
         onClearButton().assertDoesNotExist()
-        assertThat(searchQuery.value).isEqualTo("")
+        assertThat(searchQuery()).isEqualTo("")
     }
 
-    private fun setContent(): State<String> {
-        lateinit var actualSearchQuery: State<String>
+    private fun setContent(): () -> String {
+        lateinit var actualSearchQuery: () -> String
         composeTestRule.setContent {
             SearchScaffold(title = TITLE) { _, searchQuery ->
                 SideEffect {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt
new file mode 100644
index 0000000..2c60db4
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * A [BroadcastReceiver] flow for the given [intentFilter].
+ */
+fun Context.broadcastReceiverAsUserFlow(
+    intentFilter: IntentFilter,
+    userHandle: UserHandle,
+): Flow<Intent> = callbackFlow {
+    val broadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            trySend(intent)
+        }
+    }
+    registerReceiverAsUser(
+        broadcastReceiver,
+        userHandle,
+        intentFilter,
+        null,
+        null,
+        Context.RECEIVER_NOT_EXPORTED,
+    )
+
+    awaitClose { unregisterReceiver(broadcastReceiver) }
+}.conflate().flowOn(Dispatchers.Default)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
index ad907cf..7d6ee19 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
@@ -17,14 +17,14 @@
 package com.android.settingslib.spaprivileged.framework.compose
 
 import android.content.BroadcastReceiver
-import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.os.UserHandle
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import com.android.settingslib.spa.framework.compose.LifecycleEffect
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
+import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverAsUserFlow
 
 /**
  * A [BroadcastReceiver] which registered when on start and unregistered when on stop.
@@ -35,27 +35,6 @@
     userHandle: UserHandle,
     onReceive: (Intent) -> Unit,
 ) {
-    val context = LocalContext.current
-    val broadcastReceiver = remember {
-        object : BroadcastReceiver() {
-            override fun onReceive(context: Context, intent: Intent) {
-                onReceive(intent)
-            }
-        }
-    }
-    LifecycleEffect(
-        onStart = {
-            context.registerReceiverAsUser(
-                broadcastReceiver,
-                userHandle,
-                intentFilter,
-                null,
-                null,
-                Context.RECEIVER_NOT_EXPORTED,
-            )
-        },
-        onStop = {
-            context.unregisterReceiver(broadcastReceiver)
-        },
-    )
+    LocalContext.current.broadcastReceiverAsUserFlow(intentFilter, userHandle)
+        .collectLatestWithLifecycle(LocalLifecycleOwner.current, action = onReceive)
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt
index 8e702ea..8e28bf8 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt
@@ -23,21 +23,24 @@
 import kotlin.reflect.KProperty
 import kotlinx.coroutines.flow.Flow
 
-fun Context.settingsGlobalBoolean(name: String): ReadWriteProperty<Any?, Boolean> =
-    SettingsGlobalBooleanDelegate(this, name)
+fun Context.settingsGlobalBoolean(name: String, defaultValue: Boolean = false):
+    ReadWriteProperty<Any?, Boolean> = SettingsGlobalBooleanDelegate(this, name, defaultValue)
 
-fun Context.settingsGlobalBooleanFlow(name: String): Flow<Boolean> {
-    val value by settingsGlobalBoolean(name)
+fun Context.settingsGlobalBooleanFlow(name: String, defaultValue: Boolean = false): Flow<Boolean> {
+    val value by settingsGlobalBoolean(name, defaultValue)
     return settingsGlobalFlow(name) { value }
 }
 
-private class SettingsGlobalBooleanDelegate(context: Context, private val name: String) :
-    ReadWriteProperty<Any?, Boolean> {
+private class SettingsGlobalBooleanDelegate(
+    context: Context,
+    private val name: String,
+    private val defaultValue: Boolean = false,
+) : ReadWriteProperty<Any?, Boolean> {
 
     private val contentResolver: ContentResolver = context.contentResolver
 
     override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean =
-        Settings.Global.getInt(contentResolver, name, 0) != 0
+        Settings.Global.getInt(contentResolver, name, if (defaultValue) 1 else 0) != 0
 
     override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
         Settings.Global.putInt(contentResolver, name, if (value) 1 else 0)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index 7c45b64..68da143 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -65,8 +65,8 @@
 )
 
 data class AppListState(
-    val showSystem: State<Boolean>,
-    val searchQuery: State<String>,
+    val showSystem: () -> Boolean,
+    val searchQuery: () -> String,
 )
 
 data class AppListInput<T : AppRecord>(
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
index 07e4235..c69b5df 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
@@ -17,8 +17,10 @@
 package com.android.settingslib.spaprivileged.template.app
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.res.stringResource
 import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
 import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
@@ -47,13 +49,13 @@
     header: @Composable () -> Unit = {},
     appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
 ) {
-    val showSystem = rememberSaveable { mutableStateOf(false) }
+    var showSystem by rememberSaveable { mutableStateOf(false) }
     SearchScaffold(
         title = title,
         actions = {
             if (!noMoreOptions) {
                 MoreOptionsAction {
-                    ShowSystemAction(showSystem.value) { showSystem.value = it }
+                    ShowSystemAction(showSystem) { showSystem = it }
                     moreOptions()
                 }
             }
@@ -68,7 +70,7 @@
                 ),
                 listModel = listModel,
                 state = AppListState(
-                    showSystem = showSystem,
+                    showSystem = { showSystem },
                     searchQuery = searchQuery,
                 ),
                 header = header,
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt
new file mode 100644
index 0000000..dfb8e22
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+class BroadcastReceiverAsUserFlowTest {
+
+    private var registeredBroadcastReceiver: BroadcastReceiver? = null
+
+    private val context = mock<Context> {
+        on {
+            registerReceiverAsUser(
+                any(),
+                eq(USER_HANDLE),
+                eq(INTENT_FILTER),
+                isNull(),
+                isNull(),
+                eq(Context.RECEIVER_NOT_EXPORTED),
+            )
+        } doAnswer {
+            registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver
+            null
+        }
+    }
+
+    @Test
+    fun broadcastReceiverAsUserFlow_registered() = runBlocking {
+        val flow = context.broadcastReceiverAsUserFlow(INTENT_FILTER, USER_HANDLE)
+
+        flow.firstWithTimeoutOrNull()
+
+        assertThat(registeredBroadcastReceiver).isNotNull()
+    }
+
+    @Test
+    fun broadcastReceiverAsUserFlow_isCalledOnReceive() = runBlocking {
+        var onReceiveIsCalled = false
+        launch {
+            context.broadcastReceiverAsUserFlow(INTENT_FILTER, USER_HANDLE).first {
+                onReceiveIsCalled = true
+                true
+            }
+        }
+
+        delay(100)
+        registeredBroadcastReceiver!!.onReceive(context, Intent())
+        delay(100)
+
+        assertThat(onReceiveIsCalled).isTrue()
+    }
+
+    private companion object {
+        val USER_HANDLE: UserHandle = UserHandle.of(0)
+
+        val INTENT_FILTER = IntentFilter()
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
index 2c8fb66..f812f95 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -23,38 +23,32 @@
 import android.os.UserHandle
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
-import org.junit.Before
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.isNull
-import org.mockito.kotlin.whenever
+import org.mockito.kotlin.mock
 
 @RunWith(AndroidJUnit4::class)
 class DisposableBroadcastReceiverAsUserTest {
     @get:Rule
     val composeTestRule = createComposeRule()
 
-    @get:Rule
-    val mockito: MockitoRule = MockitoJUnit.rule()
-
-    @Mock
-    private lateinit var context: Context
-
     private var registeredBroadcastReceiver: BroadcastReceiver? = null
 
-    @Before
-    fun setUp() {
-        whenever(
-            context.registerReceiverAsUser(
+    private val context = mock<Context> {
+        on {
+            registerReceiverAsUser(
                 any(),
                 eq(USER_HANDLE),
                 eq(INTENT_FILTER),
@@ -62,7 +56,7 @@
                 isNull(),
                 eq(Context.RECEIVER_NOT_EXPORTED),
             )
-        ).then {
+        } doAnswer {
             registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver
             null
         }
@@ -71,7 +65,10 @@
     @Test
     fun broadcastReceiver_registered() {
         composeTestRule.setContent {
-            CompositionLocalProvider(LocalContext provides context) {
+            CompositionLocalProvider(
+                LocalContext provides context,
+                LocalLifecycleOwner provides TestLifecycleOwner(),
+            ) {
                 DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {}
             }
         }
@@ -80,10 +77,13 @@
     }
 
     @Test
-    fun broadcastReceiver_isCalledOnReceive() {
+    fun broadcastReceiver_isCalledOnReceive() = runBlocking {
         var onReceiveIsCalled = false
         composeTestRule.setContent {
-            CompositionLocalProvider(LocalContext provides context) {
+            CompositionLocalProvider(
+                LocalContext provides context,
+                LocalLifecycleOwner provides TestLifecycleOwner(),
+            ) {
                 DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {
                     onReceiveIsCalled = true
                 }
@@ -91,6 +91,7 @@
         }
 
         registeredBroadcastReceiver!!.onReceive(context, Intent())
+        delay(100)
 
         assertThat(onReceiveIsCalled).isTrue()
     }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index 840bca8..44973a7 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -19,6 +19,8 @@
 import android.content.Context
 import android.content.pm.ActivityInfo
 import android.content.pm.ApplicationInfo
+import android.content.pm.FakeFeatureFlagsImpl
+import android.content.pm.Flags
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ApplicationInfoFlags
 import android.content.pm.PackageManager.ResolveInfoFlags
@@ -29,76 +31,62 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.internal.R
-import com.android.settingslib.spaprivileged.framework.common.userManager
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Spy
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
-import android.content.pm.FakeFeatureFlagsImpl
-import android.content.pm.Flags
 
 @RunWith(AndroidJUnit4::class)
 class AppListRepositoryTest {
-    @get:Rule
-    val mockito: MockitoRule = MockitoJUnit.rule()
+    private val resources = mock<Resources> {
+        on { getStringArray(R.array.config_hideWhenDisabled_packageNames) } doReturn emptyArray()
+    }
 
-    @Spy
-    private val context: Context = ApplicationProvider.getApplicationContext()
-
-    @Mock
-    private lateinit var resources: Resources
-
-    @Mock
-    private lateinit var packageManager: PackageManager
-
-    @Mock
-    private lateinit var userManager: UserManager
-
-    private lateinit var repository: AppListRepository
-
-    @Before
-    fun setUp() {
-        whenever(context.resources).thenReturn(resources)
-        whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames))
-            .thenReturn(emptyArray())
-        whenever(context.packageManager).thenReturn(packageManager)
-        whenever(context.userManager).thenReturn(userManager)
-        whenever(packageManager.getInstalledModules(any())).thenReturn(emptyList())
-        whenever(packageManager.getHomeActivities(any())).thenAnswer {
+    private val packageManager = mock<PackageManager> {
+        on { getInstalledModules(any()) } doReturn emptyList()
+        on { getHomeActivities(any()) } doAnswer {
             @Suppress("UNCHECKED_CAST")
             val resolveInfos = it.arguments[0] as MutableList<ResolveInfo>
             resolveInfos += resolveInfoOf(packageName = HOME_APP.packageName)
             null
         }
-        whenever(
-            packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), any<Int>())
-        ).thenReturn(listOf(resolveInfoOf(packageName = IN_LAUNCHER_APP.packageName)))
-        whenever(userManager.getUserInfo(ADMIN_USER_ID)).thenReturn(UserInfo().apply {
-            flags = UserInfo.FLAG_ADMIN
-        })
-        whenever(userManager.getProfileIdsWithDisabled(ADMIN_USER_ID))
-            .thenReturn(intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID))
-
-        repository = AppListRepositoryImpl(context)
+        on { queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), any<Int>()) } doReturn
+            listOf(resolveInfoOf(packageName = IN_LAUNCHER_APP.packageName))
     }
 
+    private val mockUserManager = mock<UserManager> {
+        on { getUserInfo(ADMIN_USER_ID) } doReturn UserInfo().apply {
+            flags = UserInfo.FLAG_ADMIN
+        }
+        on { getProfileIdsWithDisabled(ADMIN_USER_ID) } doReturn
+            intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID)
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { resources } doReturn resources
+        on { packageManager } doReturn packageManager
+        on { getSystemService(UserManager::class.java) } doReturn mockUserManager
+    }
+
+    private val repository = AppListRepositoryImpl(context)
+
     private fun mockInstalledApplications(apps: List<ApplicationInfo>, userId: Int) {
-        whenever(
-            packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId))
-        ).thenReturn(apps)
+        packageManager.stub {
+            on { getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId)) } doReturn
+                apps
+        }
     }
 
     @Test
@@ -135,13 +123,13 @@
         )
 
         assertThat(appList).containsExactly(NORMAL_APP)
-        argumentCaptor<ApplicationInfoFlags> {
+        val flags = argumentCaptor<ApplicationInfoFlags> {
             verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
-            assertThat(firstValue.value).isEqualTo(
-                PackageManager.MATCH_DISABLED_COMPONENTS or
-                    PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
-            )
-        }
+        }.firstValue
+        assertThat(flags.value).isEqualTo(
+            PackageManager.MATCH_DISABLED_COMPONENTS or
+                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+        )
     }
 
     @Test
@@ -154,11 +142,10 @@
         )
 
         assertThat(appList).containsExactly(NORMAL_APP)
-        argumentCaptor<ApplicationInfoFlags> {
+        val flags = argumentCaptor<ApplicationInfoFlags> {
             verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
-            assertThat(firstValue.value and PackageManager.MATCH_ANY_USER.toLong())
-                .isGreaterThan(0L)
-        }
+        }.firstValue
+        assertThat(flags.value and PackageManager.MATCH_ANY_USER.toLong()).isGreaterThan(0L)
     }
 
     @Test
@@ -278,14 +265,14 @@
         val appList = repository.loadApps(userId = ADMIN_USER_ID)
 
         assertThat(appList).containsExactly(NORMAL_APP, ARCHIVED_APP)
-        argumentCaptor<ApplicationInfoFlags> {
+        val flags = argumentCaptor<ApplicationInfoFlags> {
             verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
-            assertThat(firstValue.value).isEqualTo(
-                (PackageManager.MATCH_DISABLED_COMPONENTS or
-                    PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() or
-                    PackageManager.MATCH_ARCHIVED_PACKAGES
-            )
-        }
+        }.firstValue
+        assertThat(flags.value).isEqualTo(
+            (PackageManager.MATCH_DISABLED_COMPONENTS or
+                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() or
+                PackageManager.MATCH_ARCHIVED_PACKAGES
+        )
     }
 
     @Test
@@ -294,13 +281,13 @@
         val appList = repository.loadApps(userId = ADMIN_USER_ID)
 
         assertThat(appList).containsExactly(NORMAL_APP)
-        argumentCaptor<ApplicationInfoFlags> {
+        val flags = argumentCaptor<ApplicationInfoFlags> {
             verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
-            assertThat(firstValue.value).isEqualTo(
-                PackageManager.MATCH_DISABLED_COMPONENTS or
-                    PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
-            )
-        }
+        }.firstValue
+        assertThat(flags.value).isEqualTo(
+            PackageManager.MATCH_DISABLED_COMPONENTS or
+                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+        )
     }
 
     @Test
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt
index 82fbee9..4d90076 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt
@@ -55,8 +55,8 @@
         val inputState by setContent()
 
         val state = inputState!!.state
-        assertThat(state.showSystem.value).isFalse()
-        assertThat(state.searchQuery.value).isEqualTo("")
+        assertThat(state.showSystem()).isFalse()
+        assertThat(state.searchQuery()).isEqualTo("")
     }
 
     @Test
@@ -67,7 +67,7 @@
         composeTestRule.onNodeWithText(context.getString(R.string.menu_show_system)).performClick()
 
         val state = inputState!!.state
-        assertThat(state.showSystem.value).isTrue()
+        assertThat(state.showSystem()).isTrue()
     }
 
     @Test
@@ -94,7 +94,7 @@
         val inputState by setContent(noMoreOptions = true)
 
         val state = inputState!!.state
-        assertThat(state.showSystem.value).isFalse()
+        assertThat(state.showSystem()).isFalse()
     }
 
     private fun setContent(
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
index 124ced6..c6409e7 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
@@ -30,7 +30,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.compose.stateOf
 import com.android.settingslib.spa.widget.ui.SpinnerOption
 import com.android.settingslib.spaprivileged.R
 import com.android.settingslib.spaprivileged.model.app.AppEntry
@@ -140,7 +139,7 @@
                 matchAnyUserForAdmin = false,
             ),
             listModel = TestAppListModel(enableGrouping = enableGrouping),
-            state = AppListState(showSystem = stateOf(false), searchQuery = stateOf("")),
+            state = AppListState(showSystem = { false }, searchQuery = { "" }),
             header = header,
             bottomPadding = 0.dp,
         )
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java
index 88dcc0d..83c106b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java
@@ -16,9 +16,16 @@
 
 package com.android.settingslib.fuelgauge;
 
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.view.accessibility.AccessibilityManager;
+
+import java.util.List;
 
 public final class BatteryUtils {
 
@@ -30,4 +37,35 @@
         return context.registerReceiver(
                 /*receiver=*/ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
     }
+
+    /** Gets the current active accessibility related packages. */
+    public static ArraySet<String> getA11yPackageNames(Context context) {
+        context = context.getApplicationContext();
+        final ArraySet<String> packageNames = new ArraySet<>();
+        final String defaultTtsPackageName = Settings.Secure.getString(
+                context.getContentResolver(), Settings.Secure.TTS_DEFAULT_SYNTH);
+        if (defaultTtsPackageName != null) {
+            packageNames.add(defaultTtsPackageName);
+        }
+        // Checks the current active packages.
+        final AccessibilityManager accessibilityManager =
+                context.getSystemService(AccessibilityManager.class);
+        if (!accessibilityManager.isEnabled()) {
+            return packageNames;
+        }
+        final List<AccessibilityServiceInfo> serviceInfoList =
+                accessibilityManager.getEnabledAccessibilityServiceList(
+                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+        if (serviceInfoList == null || serviceInfoList.isEmpty()) {
+            return packageNames;
+        }
+        for (AccessibilityServiceInfo serviceInfo : serviceInfoList) {
+            final ComponentName serviceComponent = ComponentName.unflattenFromString(
+                    serviceInfo.getId());
+            if (serviceComponent != null) {
+                packageNames.add(serviceComponent.getPackageName());
+            }
+        }
+        return packageNames;
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java
new file mode 100644
index 0000000..c3e0c0b
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.fuelgauge;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.provider.Settings;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowAccessibilityManager;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+public class BatteryUtilsTest {
+    private static final String DEFAULT_TTS_PACKAGE = "com.abc.talkback";
+    private static final String ACCESSIBILITY_PACKAGE = "com.def.talkback";
+
+    private Context mContext;
+    private AccessibilityManager mAccessibilityManager;
+    private ShadowAccessibilityManager mShadowAccessibilityManager;
+
+    @Mock
+    private AccessibilityServiceInfo mAccessibilityServiceInfo1;
+    @Mock
+    private AccessibilityServiceInfo mAccessibilityServiceInfo2;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        doReturn(mContext).when(mContext).getApplicationContext();
+        mAccessibilityManager = spy(mContext.getSystemService(AccessibilityManager.class));
+        mShadowAccessibilityManager = shadowOf(mAccessibilityManager);
+        doReturn(mAccessibilityManager).when(mContext)
+                .getSystemService(AccessibilityManager.class);
+
+        setTtsPackageName(DEFAULT_TTS_PACKAGE);
+        doReturn(Arrays.asList(mAccessibilityServiceInfo1, mAccessibilityServiceInfo2))
+                .when(mAccessibilityManager)
+                .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+        doReturn(ACCESSIBILITY_PACKAGE + "/.TalkbackService").when(mAccessibilityServiceInfo1)
+                .getId();
+        doReturn("dummy_package_name").when(mAccessibilityServiceInfo2).getId();
+    }
+
+    @Test
+    public void getBatteryIntent_registerReceiver() {
+        BatteryUtils.getBatteryIntent(mContext);
+        verify(mContext).registerReceiver(eq(null), any(IntentFilter.class));
+    }
+
+    @Test
+    public void getA11yPackageNames_returnDefaultTtsPackageName() {
+        mShadowAccessibilityManager.setEnabled(false);
+
+        assertThat(BatteryUtils.getA11yPackageNames(mContext))
+                .containsExactly(DEFAULT_TTS_PACKAGE);
+    }
+
+    @Test
+    public void getA11yPackageNames_returnExpectedPackageNames() {
+        mShadowAccessibilityManager.setEnabled(true);
+
+        assertThat(BatteryUtils.getA11yPackageNames(mContext))
+                .containsExactly(DEFAULT_TTS_PACKAGE, ACCESSIBILITY_PACKAGE);
+    }
+
+    private void setTtsPackageName(String defaultTtsPackageName) {
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.TTS_DEFAULT_SYNTH, defaultTtsPackageName);
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
index 942e915..74a282f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
@@ -21,30 +21,25 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
-import android.text.TextUtils;
 import android.view.View;
-import android.widget.Switch;
+import android.widget.CompoundButton;
 import android.widget.TextView;
 
+import androidx.test.core.app.ApplicationProvider;
+
 import com.android.settingslib.widget.mainswitch.R;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
 
 @RunWith(RobolectricTestRunner.class)
 public class MainSwitchBarTest {
 
-    private Context mContext;
-    private MainSwitchBar mBar;
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final MainSwitchBar mBar = new MainSwitchBar(mContext);
 
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mBar = new MainSwitchBar(mContext);
-    }
+    private final CompoundButton mSwitch = mBar.findViewById(android.R.id.switch_widget);
 
     @Test
     public void setChecked_true_shouldChecked() {
@@ -60,7 +55,7 @@
         mBar.setTitle(title);
         final TextView textView = ((TextView) mBar.findViewById(R.id.switch_text));
 
-        assertThat(textView.getText()).isEqualTo(title);
+        assertThat(textView.getText().toString()).isEqualTo(title);
     }
 
     @Test
@@ -69,23 +64,18 @@
 
         mBar.setTitle(title);
 
-        final Switch switchObj = mBar.getSwitch();
-        assertThat(TextUtils.isEmpty(switchObj.getContentDescription())).isTrue();
+        assertThat(mSwitch.getContentDescription()).isNull();
     }
 
     @Test
     public void getSwitch_shouldNotNull() {
-        final Switch switchObj = mBar.getSwitch();
-
-        assertThat(switchObj).isNotNull();
+        assertThat(mSwitch).isNotNull();
     }
 
     @Test
     public void getSwitch_shouldNotFocusableAndClickable() {
-        final Switch switchObj = mBar.getSwitch();
-
-        assertThat(switchObj.isFocusable()).isFalse();
-        assertThat(switchObj.isClickable()).isFalse();
+        assertThat(mSwitch.isFocusable()).isFalse();
+        assertThat(mSwitch.isClickable()).isFalse();
     }
 
     @Test
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index c7e5bf9..ed03d94 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -592,6 +592,9 @@
     <!-- Permission needed for CTS test - ConcurrencyTest#testP2pSetWfdInfo -->
     <uses-permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" />
 
+    <!-- Permission required for CTS test - CtsThreadNetworkTestCases -->
+    <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+
     <!-- Permission required for CTS tests to enable/disable rate limiting toasts. -->
     <uses-permission android:name="android.permission.MANAGE_TOAST_RATE_LIMITING" />
 
@@ -864,6 +867,7 @@
     <!-- Permissions required for CTS test - CtsVoiceInteractionTestCases -->
     <uses-permission android:name="android.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT" />
     <uses-permission android:name="android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA" />
+    <uses-permission android:name="android.permission.GET_BINDING_UID_IMPORTANCE" />
 
     <application
         android:label="@string/app_label"
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 88abf69..0e9f8b1 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -187,6 +187,8 @@
         "androidx.dynamicanimation_dynamicanimation",
         "androidx-constraintlayout_constraintlayout",
         "androidx.exifinterface_exifinterface",
+        "androidx.room_room-runtime",
+        "androidx.room_room-ktx",
         "com.google.android.material_material",
         "kotlinx_coroutines_android",
         "kotlinx_coroutines",
@@ -207,10 +209,16 @@
     ],
     manifest: "AndroidManifest.xml",
 
-    javacflags: ["-Adagger.fastInit=enabled"],
+    javacflags: [
+        "-Adagger.fastInit=enabled",
+        "-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas",
+    ],
     kotlincflags: ["-Xjvm-default=all"],
 
-    plugins: ["dagger2-compiler"],
+    plugins: [
+        "androidx.room_room-compiler-plugin",
+        "dagger2-compiler",
+    ],
 
     lint: {
         extra_check_modules: ["SystemUILintChecker"],
@@ -466,6 +474,8 @@
         "androidx.dynamicanimation_dynamicanimation",
         "androidx-constraintlayout_constraintlayout",
         "androidx.exifinterface_exifinterface",
+        "androidx.room_room-runtime",
+        "androidx.room_room-ktx",
         "kotlinx-coroutines-android",
         "kotlinx-coroutines-core",
         "kotlinx_coroutines_test",
@@ -530,7 +540,10 @@
         "--extra-packages",
         "com.android.systemui",
     ],
-    plugins: ["dagger2-compiler"],
+    plugins: [
+        "androidx.room_room-compiler-plugin",
+        "dagger2-compiler",
+    ],
     lint: {
         test: true,
         extra_check_modules: ["SystemUILintChecker"],
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 28539dd..0480b9d 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -88,20 +88,6 @@
           "include-filter": "android.permissionui.cts.CameraMicIndicatorsPermissionTest"
         }
       ]
-    },
-    {
-      "name": "SystemUIGoogleScreenshotTests",
-      "options": [
-        {
-            "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
-        },
-        {
-            "include-annotation": "android.platform.test.annotations.Postsubmit"
-        }
-      ]
     }
   ],
   
@@ -171,12 +157,6 @@
         },
         {
           "include-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.FlakyTest"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Postsubmit"
         }
       ]
     },
@@ -188,28 +168,18 @@
         },
         {
           "include-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.FlakyTest"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Postsubmit"
         }
       ]
     },
-    {      "name": "SystemUIGoogleBiometricsScreenshotTests",
+    {
+      // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
+      "name": "SystemUIGoogleBiometricsScreenshotTests",
       "options": [
         {
           "exclude-annotation": "org.junit.Ignore"
         },
         {
           "include-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.FlakyTest"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Postsubmit"
         }
       ]
     }
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
index 96e1e3f..085fc29 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -63,7 +63,7 @@
 
     private static final String TAG = "A11yMenuService";
     private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L;
-    private static final long TAKE_SCREENSHOT_DELAY_MS = 100L;
+    private static final long HIDE_UI_DELAY_MS = 100L;
 
     private static final int BRIGHTNESS_UP_INCREMENT_GAMMA =
             (int) Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);
@@ -296,7 +296,14 @@
         } else if (viewTag == ShortcutId.ID_RECENT_VALUE.ordinal()) {
             performGlobalActionInternal(GLOBAL_ACTION_RECENTS);
         } else if (viewTag == ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()) {
-            performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN);
+            if (Flags.a11yMenuHideBeforeTakingAction()) {
+                // Delay before locking the screen to give time for the UI to close.
+                mHandler.postDelayed(
+                        () -> performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN),
+                        HIDE_UI_DELAY_MS);
+            } else {
+                performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN);
+            }
         } else if (viewTag == ShortcutId.ID_QUICKSETTING_VALUE.ordinal()) {
             performGlobalActionInternal(GLOBAL_ACTION_QUICK_SETTINGS);
         } else if (viewTag == ShortcutId.ID_NOTIFICATION_VALUE.ordinal()) {
@@ -306,7 +313,7 @@
                 // Delay before taking a screenshot to give time for the UI to close.
                 mHandler.postDelayed(
                         () -> performGlobalActionInternal(GLOBAL_ACTION_TAKE_SCREENSHOT),
-                        TAKE_SCREENSHOT_DELAY_MS);
+                        HIDE_UI_DELAY_MS);
             } else {
                 performGlobalActionInternal(GLOBAL_ACTION_TAKE_SCREENSHOT);
             }
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8019b38..9700bc6 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -46,6 +46,14 @@
 }
 
 flag {
+    name: "notifications_live_data_store_refactor"
+    namespace: "systemui"
+    description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. "
+        "Should not bring any behavior changes."
+    bug: "308623704"
+}
+
+flag {
     name: "scene_container"
     namespace: "systemui"
     description: "Enables the scene container framework go/flexiglass."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 56970d7..d668c69 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -154,7 +154,7 @@
                 Bouncer(
                     viewModel = viewModel,
                     dialogFactory = dialogFactory,
-                    isUserInputAreaVisible = true,
+                    userInputAreaVisibility = UserInputAreaVisibility.FULL,
                     modifier = childModifier,
                 )
             Layout.SIDE_BY_SIDE ->
@@ -189,7 +189,7 @@
 private fun Bouncer(
     viewModel: BouncerViewModel,
     dialogFactory: BouncerSceneDialogFactory,
-    isUserInputAreaVisible: Boolean,
+    userInputAreaVisibility: UserInputAreaVisibility,
     modifier: Modifier = Modifier,
 ) {
     val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
@@ -214,12 +214,11 @@
         }
 
         Box(Modifier.weight(1f)) {
-            if (isUserInputAreaVisible) {
-                UserInputArea(
-                    viewModel = viewModel,
-                    modifier = Modifier.align(Alignment.Center),
-                )
-            }
+            UserInputArea(
+                viewModel = viewModel,
+                visibility = userInputAreaVisibility,
+                modifier = Modifier.align(Alignment.Center),
+            )
         }
 
         if (viewModel.isEmergencyButtonVisible) {
@@ -269,6 +268,7 @@
 @Composable
 private fun UserInputArea(
     viewModel: BouncerViewModel,
+    visibility: UserInputAreaVisibility,
     modifier: Modifier = Modifier,
 ) {
     val authMethodViewModel: AuthMethodBouncerViewModel? by
@@ -276,21 +276,46 @@
 
     when (val nonNullViewModel = authMethodViewModel) {
         is PinBouncerViewModel ->
-            PinBouncer(
-                viewModel = nonNullViewModel,
-                modifier = modifier,
-            )
+            when (visibility) {
+                UserInputAreaVisibility.FULL ->
+                    PinBouncer(
+                        viewModel = nonNullViewModel,
+                        modifier = modifier,
+                    )
+                UserInputAreaVisibility.INPUT_ONLY ->
+                    PinPad(
+                        viewModel = nonNullViewModel,
+                        modifier = modifier,
+                    )
+                UserInputAreaVisibility.OUTPUT_ONLY ->
+                    PinInputDisplay(
+                        viewModel = nonNullViewModel,
+                        modifier = modifier,
+                    )
+                UserInputAreaVisibility.NONE -> {}
+            }
         is PasswordBouncerViewModel ->
-            PasswordBouncer(
-                viewModel = nonNullViewModel,
-                modifier = modifier,
-            )
+            when (visibility) {
+                UserInputAreaVisibility.FULL,
+                UserInputAreaVisibility.INPUT_ONLY ->
+                    PasswordBouncer(
+                        viewModel = nonNullViewModel,
+                        modifier = modifier,
+                    )
+                else -> {}
+            }
         is PatternBouncerViewModel ->
-            PatternBouncer(
-                viewModel = nonNullViewModel,
-                modifier =
-                    Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false).then(modifier)
-            )
+            when (visibility) {
+                UserInputAreaVisibility.FULL,
+                UserInputAreaVisibility.INPUT_ONLY ->
+                    PatternBouncer(
+                        viewModel = nonNullViewModel,
+                        modifier =
+                            Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
+                                .then(modifier)
+                    )
+                else -> {}
+            }
         else -> Unit
     }
 }
@@ -435,13 +460,14 @@
             Bouncer(
                 viewModel = viewModel,
                 dialogFactory = dialogFactory,
-                isUserInputAreaVisible = false,
+                userInputAreaVisibility = UserInputAreaVisibility.OUTPUT_ONLY,
                 modifier = startContentModifier,
             )
         },
         endContent = { endContentModifier ->
             UserInputArea(
                 viewModel = viewModel,
+                visibility = UserInputAreaVisibility.INPUT_ONLY,
                 modifier = endContentModifier,
             )
         },
@@ -545,7 +571,7 @@
             Bouncer(
                 viewModel = viewModel,
                 dialogFactory = dialogFactory,
-                isUserInputAreaVisible = true,
+                userInputAreaVisibility = UserInputAreaVisibility.FULL,
                 modifier = endContentModifier,
             )
         },
@@ -574,7 +600,7 @@
         Bouncer(
             viewModel = viewModel,
             dialogFactory = dialogFactory,
-            isUserInputAreaVisible = true,
+            userInputAreaVisibility = UserInputAreaVisibility.FULL,
             modifier = Modifier.fillMaxWidth().weight(1f),
         )
     }
@@ -630,6 +656,27 @@
     SPLIT,
 }
 
+/** Enumerates all supported user-input area visibilities. */
+private enum class UserInputAreaVisibility {
+    /**
+     * The entire user input area is shown, including where the user enters input and where it's
+     * reflected to the user.
+     */
+    FULL,
+    /**
+     * Only the area where the user enters the input is shown; the area where the input is reflected
+     * back to the user is not shown.
+     */
+    INPUT_ONLY,
+    /**
+     * Only the area where the input is reflected back to the user is shown; the area where the
+     * input is entered by the user is not shown.
+     */
+    OUTPUT_ONLY,
+    /** The entire user input area is hidden. */
+    NONE,
+}
+
 /**
  * Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of
  * the two reaches a stopping point but `0` in the middle of the transition.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 6491b70..84e0167 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -55,12 +55,12 @@
 import com.android.compose.animation.Easings
 import com.android.compose.grid.VerticalGrid
 import com.android.compose.modifiers.thenIf
-import com.android.systemui.res.R
 import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.res.R
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.DurationUnit
 import kotlinx.coroutines.async
@@ -93,7 +93,10 @@
 }
 
 @Composable
-private fun PinPad(viewModel: PinBouncerViewModel) {
+fun PinPad(
+    viewModel: PinBouncerViewModel,
+    modifier: Modifier = Modifier,
+) {
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
     val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState()
@@ -112,6 +115,7 @@
         columns = 3,
         verticalSpacing = 12.dp,
         horizontalSpacing = 20.dp,
+        modifier = modifier,
     ) {
         repeat(9) { index ->
             DigitButton(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
index 055ece3..814ea31 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -51,10 +51,10 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.Easings
 import com.android.keyguard.PinShapeAdapter
-import com.android.systemui.res.R
 import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel
+import com.android.systemui.res.R
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
@@ -65,7 +65,10 @@
 import kotlinx.coroutines.launch
 
 @Composable
-fun PinInputDisplay(viewModel: PinBouncerViewModel) {
+fun PinInputDisplay(
+    viewModel: PinBouncerViewModel,
+    modifier: Modifier = Modifier,
+) {
     val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsState()
     val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes)
 
@@ -81,8 +84,8 @@
     // unifying into a single, more complex implementation.
 
     when (val length = hintedPinLength) {
-        null -> RegularPinInputDisplay(viewModel, shapeAnimations)
-        else -> HintingPinInputDisplay(viewModel, shapeAnimations, length)
+        null -> RegularPinInputDisplay(viewModel, shapeAnimations, modifier)
+        else -> HintingPinInputDisplay(viewModel, shapeAnimations, length, modifier)
     }
 }
 
@@ -97,6 +100,7 @@
     viewModel: PinBouncerViewModel,
     shapeAnimations: ShapeAnimations,
     hintedPinLength: Int,
+    modifier: Modifier = Modifier,
 ) {
     val pinInput: PinInputViewModel by viewModel.pinInput.collectAsState()
     // [ClearAll] marker pointing at the beginning of the current pin input.
@@ -151,7 +155,7 @@
     LaunchedEffect(Unit) { playAnimation = true }
 
     val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
-    Row(modifier = Modifier.heightIn(min = shapeAnimations.shapeSize)) {
+    Row(modifier = modifier.heightIn(min = shapeAnimations.shapeSize)) {
         pinEntryDrawable.forEachIndexed { index, drawable ->
             // Key the loop by [index] and [drawable], so that updating a shape drawable at the same
             // index will play the new animation (by remembering a new [atEnd]).
@@ -183,6 +187,7 @@
 private fun RegularPinInputDisplay(
     viewModel: PinBouncerViewModel,
     shapeAnimations: ShapeAnimations,
+    modifier: Modifier = Modifier,
 ) {
     // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from
     // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove
@@ -226,7 +231,7 @@
             }
     }
 
-    pinInputRow.Content()
+    pinInputRow.Content(modifier)
 }
 
 private class PinInputRow(
@@ -235,10 +240,11 @@
     private val entries = mutableStateListOf<PinInputEntry>()
 
     @Composable
-    fun Content() {
+    fun Content(modifier: Modifier) {
         Row(
             modifier =
-                Modifier.heightIn(min = shapeAnimations.shapeSize)
+                modifier
+                    .heightIn(min = shapeAnimations.shapeSize)
                     // Pins overflowing horizontally should still be shown as scrolling.
                     .wrapContentSize(unbounded = true),
         ) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 87a8c35..17726ab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.communal.ui.compose
 
 import android.appwidget.AppWidgetHostView
@@ -12,19 +28,26 @@
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
 import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.ui.model.CommunalContentUiModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.res.R
 
 @Composable
 fun CommunalHub(
@@ -64,10 +87,17 @@
                     ContentCard(
                         modifier = Modifier.size(Dimensions.CardWidth, widget.size.dp()),
                         model = widget,
+                        deleteOnClick = viewModel::onDeleteWidget
                     )
                 }
             }
         }
+        IconButton(onClick = viewModel::onOpenWidgetPicker) {
+            Icon(
+                Icons.Default.Add,
+                LocalContext.current.getString(R.string.button_to_open_widget_picker)
+            )
+        }
     }
 }
 
@@ -80,19 +110,36 @@
 @Composable
 private fun ContentCard(
     model: CommunalContentUiModel,
+    deleteOnClick: (id: Int) -> Unit,
     modifier: Modifier = Modifier,
 ) {
-    AndroidView(
-        modifier = modifier,
-        factory = {
-            model.view.apply {
-                if (this is AppWidgetHostView) {
-                    val size = SizeF(Dimensions.CardWidth.value, model.size.dp().value)
-                    updateAppWidgetSize(Bundle.EMPTY, listOf(size))
-                }
+    // TODO(b/309009246): update background color
+    Box(
+        modifier = modifier.fillMaxSize().background(Color.White),
+    ) {
+        // TODO(b/308148193): this will be cleaned up soon once the change to convert to
+        // CommunalContentUiModel interface is merged
+        val widgetId = getWidgetId(model.id)
+        widgetId?.let {
+            IconButton(onClick = { deleteOnClick(it) }) {
+                Icon(
+                    Icons.Default.Close,
+                    LocalContext.current.getString(R.string.button_to_remove_widget)
+                )
             }
-        },
-    )
+        }
+        AndroidView(
+            modifier = modifier,
+            factory = {
+                model.view.apply {
+                    if (this is AppWidgetHostView) {
+                        val size = SizeF(Dimensions.CardWidth.value, model.size.dp().value)
+                        updateAppWidgetSize(Bundle.EMPTY, listOf(size))
+                    }
+                }
+            },
+        )
+    }
 }
 
 private fun CommunalContentSize.dp(): Dp {
@@ -103,6 +150,10 @@
     }
 }
 
+private fun getWidgetId(id: String): Int? {
+    return if (id.startsWith("widget_")) id.substring("widget_".length).toInt() else null
+}
+
 // Sizes for the tutorial placeholders.
 private val tutorialContentSizes =
     listOf(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 041fc48..009f8bb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -36,7 +36,7 @@
 fun SceneScope.animateSharedIntAsState(
     value: Int,
     key: ValueKey,
-    element: ElementKey,
+    element: ElementKey?,
     canOverflow: Boolean = true,
 ): State<Int> {
     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
@@ -65,7 +65,7 @@
 fun SceneScope.animateSharedFloatAsState(
     value: Float,
     key: ValueKey,
-    element: ElementKey,
+    element: ElementKey?,
     canOverflow: Boolean = true,
 ): State<Float> {
     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
@@ -94,7 +94,7 @@
 fun SceneScope.animateSharedDpAsState(
     value: Dp,
     key: ValueKey,
-    element: ElementKey,
+    element: ElementKey?,
     canOverflow: Boolean = true,
 ): State<Dp> {
     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
@@ -123,7 +123,7 @@
 fun SceneScope.animateSharedColorAsState(
     value: Color,
     key: ValueKey,
-    element: ElementKey,
+    element: ElementKey?,
 ): State<Color> {
     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false)
 }
@@ -145,7 +145,7 @@
 internal fun <T> animateSharedValueAsState(
     layoutImpl: SceneTransitionLayoutImpl,
     scene: Scene,
-    element: Element,
+    element: Element?,
     key: ValueKey,
     value: T,
     lerp: (T, T, Float) -> T,
@@ -153,9 +153,9 @@
 ): State<T> {
     val sharedValue =
         Snapshot.withoutReadObservation {
-            element.sceneValues.getValue(scene.key).sharedValues.getOrPut(key) {
-                Element.SharedValue(key, value)
-            } as Element.SharedValue<T>
+            val sharedValues =
+                element?.sceneValues?.getValue(scene.key)?.sharedValues ?: scene.sharedValues
+            sharedValues.getOrPut(key) { Element.SharedValue(key, value) } as Element.SharedValue<T>
         }
 
     if (value != sharedValue.value) {
@@ -169,7 +169,7 @@
 
 private fun <T> computeValue(
     layoutImpl: SceneTransitionLayoutImpl,
-    element: Element,
+    element: Element?,
     sharedValue: Element.SharedValue<T>,
     lerp: (T, T, Float) -> T,
     canOverflow: Boolean,
@@ -184,8 +184,14 @@
     }
 
     fun sceneValue(scene: SceneKey): Element.SharedValue<T>? {
-        val sceneValues = element.sceneValues[scene] ?: return null
-        val value = sceneValues.sharedValues[sharedValue.key] ?: return null
+        val sharedValues =
+            if (element == null) {
+                layoutImpl.scene(scene).sharedValues
+            } else {
+                element.sceneValues[scene]?.sharedValues
+            }
+                ?: return null
+        val value = sharedValues[sharedValue.key] ?: return null
         return value as Element.SharedValue<T>
     }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index abc62c4..eb10afc 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
 import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.isSpecified
@@ -110,13 +111,12 @@
 }
 
 /** The implementation of [SceneScope.element]. */
-@Composable
 @OptIn(ExperimentalComposeUiApi::class)
 internal fun Modifier.element(
     layoutImpl: SceneTransitionLayoutImpl,
     scene: Scene,
     key: ElementKey,
-): Modifier {
+): Modifier = composed {
     val sceneValues = remember(scene, key) { Element.TargetValues() }
     val element =
         // Get the element associated to [key] if it was already composed in another scene,
@@ -160,7 +160,7 @@
         }
     }
 
-    return drawWithContent {
+    drawWithContent {
             if (shouldDrawElement(layoutImpl, scene, element)) {
                 drawContent()
             }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 97d3fff..d0a5f5b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -23,11 +23,11 @@
 import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
 import androidx.compose.foundation.gestures.horizontalDrag
 import androidx.compose.foundation.gestures.verticalDrag
-import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerId
@@ -56,7 +56,6 @@
  * change in the future.
  */
 // TODO(b/291055080): Migrate to the Modifier.Node API.
-@Composable
 internal fun Modifier.multiPointerDraggable(
     orientation: Orientation,
     enabled: Boolean,
@@ -64,7 +63,7 @@
     onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
     onDragDelta: (Float) -> Unit,
     onDragStopped: (velocity: Float) -> Unit,
-): Modifier {
+): Modifier = composed {
     val onDragStarted by rememberUpdatedState(onDragStarted)
     val onDragStopped by rememberUpdatedState(onDragStopped)
     val onDragDelta by rememberUpdatedState(onDragDelta)
@@ -77,7 +76,7 @@
             Velocity(maxF, maxF)
         }
 
-    return this.pointerInput(enabled, orientation, maxFlingVelocity) {
+    pointerInput(enabled, orientation, maxFlingVelocity) {
         if (!enabled) {
             return@pointerInput
         }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 3fd6828..2e50a71 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.platform.testTag
@@ -44,6 +45,9 @@
     var zIndex by mutableFloatStateOf(zIndex)
     var size by mutableStateOf(IntSize.Zero)
 
+    /** The shared values in this scene that are not tied to a specific element. */
+    val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>()
+
     @Composable
     fun Content(modifier: Modifier = Modifier) {
         Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) {
@@ -60,7 +64,6 @@
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val scene: Scene,
 ) : SceneScope {
-    @Composable
     override fun Modifier.element(key: ElementKey): Modifier {
         return element(layoutImpl, scene, key)
     }
@@ -69,16 +72,18 @@
     override fun <T> animateSharedValueAsState(
         value: T,
         key: ValueKey,
-        element: ElementKey,
+        element: ElementKey?,
         lerp: (T, T, Float) -> T,
         canOverflow: Boolean
     ): State<T> {
         val element =
-            layoutImpl.elements[element]
-                ?: error(
-                    "Element $element is not composed. Make sure to call animateSharedXAsState " +
-                        "*after* Modifier.element(key)."
-                )
+            element?.let { key ->
+                layoutImpl.elements[key]
+                    ?: error(
+                        "Element $key is not composed. Make sure to call animateSharedXAsState " +
+                            "*after* Modifier.element(key)."
+                    )
+            }
 
         return animateSharedValueAsState(
             layoutImpl,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
new file mode 100644
index 0000000..7563e27
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -0,0 +1,659 @@
+package com.android.compose.animation.scene
+
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+@VisibleForTesting
+class SceneGestureHandler(
+    private val layoutImpl: SceneTransitionLayoutImpl,
+    internal val orientation: Orientation,
+    private val coroutineScope: CoroutineScope,
+) : GestureHandler {
+    override val draggable: DraggableHandler = SceneDraggableHandler(this)
+
+    override val nestedScroll: SceneNestedScrollHandler = SceneNestedScrollHandler(this)
+
+    private var transitionState
+        get() = layoutImpl.state.transitionState
+        set(value) {
+            layoutImpl.state.transitionState = value
+        }
+
+    /**
+     * The transition controlled by this gesture handler. It will be set as the [transitionState] in
+     * the [SceneTransitionLayoutImpl] whenever this handler is driving the current transition.
+     *
+     * Note: the initialScene here does not matter, it's only used for initializing the transition
+     * and will be replaced when a drag event starts.
+     */
+    private val swipeTransition = SwipeTransition(initialScene = currentScene)
+
+    internal val currentScene: Scene
+        get() = layoutImpl.scene(transitionState.currentScene)
+
+    @VisibleForTesting
+    val isDrivingTransition
+        get() = transitionState == swipeTransition
+
+    @VisibleForTesting
+    var isAnimatingOffset
+        get() = swipeTransition.isAnimatingOffset
+        private set(value) {
+            swipeTransition.isAnimatingOffset = value
+        }
+
+    internal val swipeTransitionToScene
+        get() = swipeTransition._toScene
+
+    /**
+     * The velocity threshold at which the intent of the user is to swipe up or down. It is the same
+     * as SwipeableV2Defaults.VelocityThreshold.
+     */
+    @VisibleForTesting val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
+
+    /**
+     * The positional threshold at which the intent of the user is to swipe to the next scene. It is
+     * the same as SwipeableV2Defaults.PositionalThreshold.
+     */
+    private val positionalThreshold = with(layoutImpl.density) { 56.dp.toPx() }
+
+    internal var gestureWithPriority: Any? = null
+
+    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
+        if (isDrivingTransition) {
+            // This [transition] was already driving the animation: simply take over it.
+            // Stop animating and start from where the current offset.
+            swipeTransition.stopOffsetAnimation()
+            return
+        }
+
+        val transition = transitionState
+        if (transition is TransitionState.Transition) {
+            // TODO(b/290184746): Better handle interruptions here if state != idle.
+            Log.w(
+                TAG,
+                "start from TransitionState.Transition is not fully supported: from" +
+                    " ${transition.fromScene} to ${transition.toScene} " +
+                    "(progress ${transition.progress})"
+            )
+        }
+
+        val fromScene = currentScene
+
+        swipeTransition._currentScene = fromScene
+        swipeTransition._fromScene = fromScene
+
+        // We don't know where we are transitioning to yet given that the drag just started, so set
+        // it to fromScene, which will effectively be treated the same as Idle(fromScene).
+        swipeTransition._toScene = fromScene
+
+        swipeTransition.stopOffsetAnimation()
+        swipeTransition.dragOffset = 0f
+
+        // Use the layout size in the swipe orientation for swipe distance.
+        // TODO(b/290184746): Also handle custom distances for transitions. With smaller distances,
+        // we will also have to make sure that we correctly handle overscroll.
+        swipeTransition.absoluteDistance =
+            when (orientation) {
+                Orientation.Horizontal -> layoutImpl.size.width
+                Orientation.Vertical -> layoutImpl.size.height
+            }.toFloat()
+
+        val fromEdge =
+            startedPosition?.let { position ->
+                layoutImpl.edgeDetector.edge(
+                    layoutImpl.size,
+                    position.round(),
+                    layoutImpl.density,
+                    orientation,
+                )
+            }
+
+        swipeTransition.actionUpOrLeft =
+            Swipe(
+                direction =
+                    when (orientation) {
+                        Orientation.Horizontal -> SwipeDirection.Left
+                        Orientation.Vertical -> SwipeDirection.Up
+                    },
+                pointerCount = pointersDown,
+                fromEdge = fromEdge,
+            )
+
+        swipeTransition.actionDownOrRight =
+            Swipe(
+                direction =
+                    when (orientation) {
+                        Orientation.Horizontal -> SwipeDirection.Right
+                        Orientation.Vertical -> SwipeDirection.Down
+                    },
+                pointerCount = pointersDown,
+                fromEdge = fromEdge,
+            )
+
+        if (fromEdge == null) {
+            swipeTransition.actionUpOrLeftNoEdge = null
+            swipeTransition.actionDownOrRightNoEdge = null
+        } else {
+            swipeTransition.actionUpOrLeftNoEdge =
+                (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
+            swipeTransition.actionDownOrRightNoEdge =
+                (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
+        }
+
+        if (swipeTransition.absoluteDistance > 0f) {
+            transitionState = swipeTransition
+        }
+    }
+
+    internal fun onDrag(delta: Float) {
+        if (delta == 0f) return
+
+        swipeTransition.dragOffset += delta
+
+        // First check transition.fromScene should be changed for the case where the user quickly
+        // swiped twice in a row to accelerate the transition and go from A => B then B => C really
+        // fast.
+        maybeHandleAcceleratedSwipe()
+
+        val offset = swipeTransition.dragOffset
+        val fromScene = swipeTransition._fromScene
+
+        // Compute the target scene depending on the current offset.
+        val target = fromScene.findTargetSceneAndDistance(offset)
+
+        if (swipeTransition._toScene.key != target.sceneKey) {
+            swipeTransition._toScene = layoutImpl.scenes.getValue(target.sceneKey)
+        }
+
+        if (swipeTransition._distance != target.distance) {
+            swipeTransition._distance = target.distance
+        }
+    }
+
+    /**
+     * Change fromScene in the case where the user quickly swiped multiple times in the same
+     * direction to accelerate the transition from A => B then B => C.
+     */
+    private fun maybeHandleAcceleratedSwipe() {
+        val toScene = swipeTransition._toScene
+        val fromScene = swipeTransition._fromScene
+
+        // If the swipe was not committed, don't do anything.
+        if (fromScene == toScene || swipeTransition._currentScene != toScene) {
+            return
+        }
+
+        // If the offset is past the distance then let's change fromScene so that the user can swipe
+        // to the next screen or go back to the previous one.
+        val offset = swipeTransition.dragOffset
+        val absoluteDistance = swipeTransition.absoluteDistance
+        if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
+            swipeTransition.dragOffset += absoluteDistance
+            swipeTransition._fromScene = toScene
+        } else if (
+            offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
+        ) {
+            swipeTransition.dragOffset -= absoluteDistance
+            swipeTransition._fromScene = toScene
+        }
+
+        // Important note: toScene and distance will be updated right after this function is called,
+        // using fromScene and dragOffset.
+    }
+
+    private class TargetScene(
+        val sceneKey: SceneKey,
+        val distance: Float,
+    )
+
+    private fun Scene.findTargetSceneAndDistance(directionOffset: Float): TargetScene {
+        val upOrLeft = swipeTransition.upOrLeft(this)
+        val downOrRight = swipeTransition.downOrRight(this)
+
+        // Compute the target scene depending on the current offset.
+        return when {
+            directionOffset < 0f && upOrLeft != null -> {
+                TargetScene(
+                    sceneKey = upOrLeft,
+                    distance = -swipeTransition.absoluteDistance,
+                )
+            }
+            directionOffset > 0f && downOrRight != null -> {
+                TargetScene(
+                    sceneKey = downOrRight,
+                    distance = swipeTransition.absoluteDistance,
+                )
+            }
+            else -> {
+                TargetScene(
+                    sceneKey = key,
+                    distance = 0f,
+                )
+            }
+        }
+    }
+
+    internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
+        // The state was changed since the drag started; don't do anything.
+        if (!isDrivingTransition) {
+            return
+        }
+
+        fun animateTo(targetScene: Scene, targetOffset: Float) {
+            // If the effective current scene changed, it should be reflected right now in the
+            // current scene state, even before the settle animation is ongoing. That way all the
+            // swipeables and back handlers will be refreshed and the user can for instance quickly
+            // swipe vertically from A => B then horizontally from B => C, or swipe from A => B then
+            // immediately go back B => A.
+            if (targetScene != swipeTransition._currentScene) {
+                swipeTransition._currentScene = targetScene
+                layoutImpl.onChangeScene(targetScene.key)
+            }
+
+            animateOffset(
+                initialVelocity = velocity,
+                targetOffset = targetOffset,
+                targetScene = targetScene.key
+            )
+        }
+
+        val fromScene = swipeTransition._fromScene
+        if (canChangeScene) {
+            // If we are halfway between two scenes, we check what the target will be based on the
+            // velocity and offset of the transition, then we launch the animation.
+
+            val toScene = swipeTransition._toScene
+            if (fromScene == toScene) {
+                // We were not animating.
+                transitionState = TransitionState.Idle(fromScene.key)
+                return
+            }
+
+            // Compute the destination scene (and therefore offset) to settle in.
+            val offset = swipeTransition.dragOffset
+            val distance = swipeTransition.distance
+            if (
+                shouldCommitSwipe(
+                    offset,
+                    distance,
+                    velocity,
+                    wasCommitted = swipeTransition._currentScene == toScene,
+                )
+            ) {
+                // Animate to the next scene
+                animateTo(targetScene = toScene, targetOffset = distance)
+            } else {
+                // Animate to the initial scene
+                animateTo(targetScene = fromScene, targetOffset = 0f)
+            }
+        } else {
+            // We are doing an overscroll animation between scenes. In this case, we can also start
+            // from the idle position.
+
+            val startFromIdlePosition = swipeTransition.dragOffset == 0f
+
+            if (startFromIdlePosition) {
+                // If there is a next scene, we start the overscroll animation.
+                val target = fromScene.findTargetSceneAndDistance(velocity)
+                val isValidTarget = target.distance != 0f && target.sceneKey != fromScene.key
+                if (isValidTarget) {
+                    swipeTransition._toScene = layoutImpl.scene(target.sceneKey)
+                    swipeTransition._distance = target.distance
+
+                    animateTo(targetScene = fromScene, targetOffset = 0f)
+                } else {
+                    // We will not animate
+                    transitionState = TransitionState.Idle(fromScene.key)
+                }
+            } else {
+                // We were between two scenes: animate to the initial scene.
+                animateTo(targetScene = fromScene, targetOffset = 0f)
+            }
+        }
+    }
+
+    /**
+     * Whether the swipe to the target scene should be committed or not. This is inspired by
+     * SwipeableV2.computeTarget().
+     */
+    private fun shouldCommitSwipe(
+        offset: Float,
+        distance: Float,
+        velocity: Float,
+        wasCommitted: Boolean,
+    ): Boolean {
+        fun isCloserToTarget(): Boolean {
+            return (offset - distance).absoluteValue < offset.absoluteValue
+        }
+
+        // Swiping up or left.
+        if (distance < 0f) {
+            return if (offset > 0f || velocity >= velocityThreshold) {
+                false
+            } else {
+                velocity <= -velocityThreshold ||
+                    (offset <= -positionalThreshold && !wasCommitted) ||
+                    isCloserToTarget()
+            }
+        }
+
+        // Swiping down or right.
+        return if (offset < 0f || velocity <= -velocityThreshold) {
+            false
+        } else {
+            velocity >= velocityThreshold ||
+                (offset >= positionalThreshold && !wasCommitted) ||
+                isCloserToTarget()
+        }
+    }
+
+    private fun animateOffset(
+        initialVelocity: Float,
+        targetOffset: Float,
+        targetScene: SceneKey,
+    ) {
+        swipeTransition.startOffsetAnimation {
+            coroutineScope.launch {
+                if (!isAnimatingOffset) {
+                    swipeTransition.offsetAnimatable.snapTo(swipeTransition.dragOffset)
+                }
+                isAnimatingOffset = true
+
+                swipeTransition.offsetAnimatable.animateTo(
+                    targetOffset,
+                    // TODO(b/290184746): Make this spring spec configurable.
+                    spring(
+                        stiffness = Spring.StiffnessMediumLow,
+                        visibilityThreshold = OffsetVisibilityThreshold
+                    ),
+                    initialVelocity = initialVelocity,
+                )
+
+                isAnimatingOffset = false
+
+                // Now that the animation is done, the state should be idle. Note that if the state
+                // was changed since this animation started, some external code changed it and we
+                // shouldn't do anything here. Note also that this job will be cancelled in the case
+                // where the user intercepts this swipe.
+                if (isDrivingTransition) {
+                    transitionState = TransitionState.Idle(targetScene)
+                }
+            }
+        }
+    }
+
+    private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
+        var _currentScene by mutableStateOf(initialScene)
+        override val currentScene: SceneKey
+            get() = _currentScene.key
+
+        var _fromScene by mutableStateOf(initialScene)
+        override val fromScene: SceneKey
+            get() = _fromScene.key
+
+        var _toScene by mutableStateOf(initialScene)
+        override val toScene: SceneKey
+            get() = _toScene.key
+
+        override val progress: Float
+            get() {
+                val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+                if (distance == 0f) {
+                    // This can happen only if fromScene == toScene.
+                    error(
+                        "Transition.progress should be called only when Transition.fromScene != " +
+                            "Transition.toScene"
+                    )
+                }
+                return offset / distance
+            }
+
+        override val isInitiatedByUserInput = true
+
+        /** The current offset caused by the drag gesture. */
+        var dragOffset by mutableFloatStateOf(0f)
+
+        /**
+         * Whether the offset is animated (the user lifted their finger) or if it is driven by
+         * gesture.
+         */
+        var isAnimatingOffset by mutableStateOf(false)
+
+        // If we are not animating offset, it means the offset is being driven by the user's finger.
+        override val isUserInputOngoing: Boolean
+            get() = !isAnimatingOffset
+
+        /** The animatable used to animate the offset once the user lifted its finger. */
+        val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
+
+        /** Job to check that there is at most one offset animation in progress. */
+        private var offsetAnimationJob: Job? = null
+
+        /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
+        fun startOffsetAnimation(job: () -> Job) {
+            stopOffsetAnimation()
+            offsetAnimationJob = job()
+        }
+
+        /** Stops any ongoing offset animation. */
+        fun stopOffsetAnimation() {
+            offsetAnimationJob?.cancel()
+
+            if (isAnimatingOffset) {
+                isAnimatingOffset = false
+                dragOffset = offsetAnimatable.value
+            }
+        }
+
+        /** The absolute distance between [fromScene] and [toScene]. */
+        var absoluteDistance = 0f
+
+        /**
+         * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
+         * above or to the left of [toScene].
+         */
+        var _distance by mutableFloatStateOf(0f)
+        val distance: Float
+            get() = _distance
+
+        /** The [UserAction]s associated to this swipe. */
+        var actionUpOrLeft: UserAction = Back
+        var actionDownOrRight: UserAction = Back
+        var actionUpOrLeftNoEdge: UserAction? = null
+        var actionDownOrRightNoEdge: UserAction? = null
+
+        fun upOrLeft(scene: Scene): SceneKey? {
+            return scene.userActions[actionUpOrLeft]
+                ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
+        }
+
+        fun downOrRight(scene: Scene): SceneKey? {
+            return scene.userActions[actionDownOrRight]
+                ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
+        }
+    }
+
+    companion object {
+        private const val TAG = "SceneGestureHandler"
+    }
+}
+
+private class SceneDraggableHandler(
+    private val gestureHandler: SceneGestureHandler,
+) : DraggableHandler {
+    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
+        gestureHandler.gestureWithPriority = this
+        gestureHandler.onDragStarted(pointersDown, startedPosition)
+    }
+
+    override fun onDelta(pixels: Float) {
+        if (gestureHandler.gestureWithPriority == this) {
+            gestureHandler.onDrag(delta = pixels)
+        }
+    }
+
+    override fun onDragStopped(velocity: Float) {
+        if (gestureHandler.gestureWithPriority == this) {
+            gestureHandler.gestureWithPriority = null
+            gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
+        }
+    }
+}
+
+@VisibleForTesting
+class SceneNestedScrollHandler(
+    private val gestureHandler: SceneGestureHandler,
+) : NestedScrollHandler {
+    override val connection: PriorityNestedScrollConnection = nestedScrollConnection()
+
+    private fun Offset.toAmount() =
+        when (gestureHandler.orientation) {
+            Orientation.Horizontal -> x
+            Orientation.Vertical -> y
+        }
+
+    private fun Velocity.toAmount() =
+        when (gestureHandler.orientation) {
+            Orientation.Horizontal -> x
+            Orientation.Vertical -> y
+        }
+
+    private fun Float.toOffset() =
+        when (gestureHandler.orientation) {
+            Orientation.Horizontal -> Offset(x = this, y = 0f)
+            Orientation.Vertical -> Offset(x = 0f, y = this)
+        }
+
+    private fun nestedScrollConnection(): PriorityNestedScrollConnection {
+        // The next potential scene is calculated during the canStart
+        var nextScene: SceneKey? = null
+
+        // This is the scene on which we will have priority during the scroll gesture.
+        var priorityScene: SceneKey? = null
+
+        // If we performed a long gesture before entering priority mode, we would have to avoid
+        // moving on to the next scene.
+        var gestureStartedOnNestedChild = false
+
+        val actionUpOrLeft =
+            Swipe(
+                direction =
+                    when (gestureHandler.orientation) {
+                        Orientation.Horizontal -> SwipeDirection.Left
+                        Orientation.Vertical -> SwipeDirection.Up
+                    },
+                pointerCount = 1,
+            )
+
+        val actionDownOrRight =
+            Swipe(
+                direction =
+                    when (gestureHandler.orientation) {
+                        Orientation.Horizontal -> SwipeDirection.Right
+                        Orientation.Vertical -> SwipeDirection.Down
+                    },
+                pointerCount = 1,
+            )
+
+        fun findNextScene(amount: Float): SceneKey? {
+            val fromScene = gestureHandler.currentScene
+            return when {
+                amount < 0f -> fromScene.userActions[actionUpOrLeft]
+                amount > 0f -> fromScene.userActions[actionDownOrRight]
+                else -> null
+            }
+        }
+
+        return PriorityNestedScrollConnection(
+            canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+                gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero
+
+                val canInterceptPreScroll =
+                    gestureHandler.isDrivingTransition &&
+                        !gestureStartedOnNestedChild &&
+                        offsetAvailable.toAmount() != 0f
+
+                if (!canInterceptPreScroll) return@PriorityNestedScrollConnection false
+
+                nextScene = gestureHandler.swipeTransitionToScene.key
+
+                true
+            },
+            canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
+                val amount = offsetAvailable.toAmount()
+                if (amount == 0f) return@PriorityNestedScrollConnection false
+
+                gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero
+                nextScene = findNextScene(amount)
+                nextScene != null
+            },
+            canStartPostFling = { velocityAvailable ->
+                val amount = velocityAvailable.toAmount()
+                if (amount == 0f) return@PriorityNestedScrollConnection false
+
+                // We could start an overscroll animation
+                gestureStartedOnNestedChild = true
+                nextScene = findNextScene(amount)
+                nextScene != null
+            },
+            canContinueScroll = { priorityScene == gestureHandler.swipeTransitionToScene.key },
+            onStart = {
+                gestureHandler.gestureWithPriority = this
+                priorityScene = nextScene
+                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
+            },
+            onScroll = { offsetAvailable ->
+                if (gestureHandler.gestureWithPriority != this) {
+                    return@PriorityNestedScrollConnection Offset.Zero
+                }
+
+                val amount = offsetAvailable.toAmount()
+
+                // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
+                // initiated in a nested child.
+                gestureHandler.onDrag(amount)
+
+                amount.toOffset()
+            },
+            onStop = { velocityAvailable ->
+                if (gestureHandler.gestureWithPriority != this) {
+                    return@PriorityNestedScrollConnection Velocity.Zero
+                }
+
+                priorityScene = null
+
+                gestureHandler.onDragStopped(
+                    velocity = velocityAvailable.toAmount(),
+                    canChangeScene = !gestureStartedOnNestedChild
+                )
+
+                // The onDragStopped animation consumes any remaining velocity.
+                velocityAvailable
+            },
+        )
+    }
+}
+
+/**
+ * The number of pixels below which there won't be a visible difference in the transition and from
+ * which the animation can stop.
+ */
+private const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 1f38e70..35d1f90 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -117,7 +117,7 @@
      * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable
      *   constraint.
      */
-    @Composable fun Modifier.element(key: ElementKey): Modifier
+    fun Modifier.element(key: ElementKey): Modifier
 
     /**
      * Create a *movable* element identified by [key].
@@ -142,7 +142,9 @@
      *
      * @param value the value of this shared value in the current scene.
      * @param key the key of this shared value.
-     * @param element the element associated with this value.
+     * @param element the element associated with this value. If `null`, this value will be
+     *   associated at the scene level, which means that [key] should be used maximum once in the
+     *   same scene.
      * @param lerp the *linear* interpolation function that should be used to interpolate between
      *   two different values. Note that it has to be linear because the [fraction] passed to this
      *   interpolator is already interpolated.
@@ -157,7 +159,7 @@
     fun <T> animateSharedValueAsState(
         value: T,
         key: ValueKey,
-        element: ElementKey,
+        element: ElementKey?,
         lerp: (start: T, stop: T, fraction: Float) -> T,
         canOverflow: Boolean,
     ): State<T>
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index fd62659..6618eb0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -30,7 +30,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateMap
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.layout.LookaheadScope
@@ -131,15 +130,19 @@
     }
 
     @Composable
-    @OptIn(ExperimentalComposeUiApi::class)
     internal fun Content(modifier: Modifier) {
+        val horizontalGestureHandler =
+            rememberSceneGestureHandler(layoutImpl = this, Orientation.Horizontal)
+        val verticalGestureHandler =
+            rememberSceneGestureHandler(layoutImpl = this, Orientation.Vertical)
+
         Box(
             modifier
                 // Handle horizontal and vertical swipes on this layout.
                 // Note: order here is important and will give a slight priority to the vertical
                 // swipes.
-                .swipeToScene(layoutImpl = this, Orientation.Horizontal)
-                .swipeToScene(layoutImpl = this, Orientation.Vertical)
+                .swipeToScene(horizontalGestureHandler)
+                .swipeToScene(verticalGestureHandler)
                 .onSizeChanged { size = it }
         ) {
             LookaheadScope {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 877ac09..8980df8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -16,47 +16,24 @@
 
 package com.android.compose.animation.scene
 
-import android.util.Log
-import androidx.annotation.VisibleForTesting
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
-import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import kotlin.math.absoluteValue
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
 
 /**
  * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
  */
-@Composable
-internal fun Modifier.swipeToScene(
-    layoutImpl: SceneTransitionLayoutImpl,
-    orientation: Orientation,
-): Modifier {
-    val gestureHandler = rememberSceneGestureHandler(layoutImpl, orientation)
-
+internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifier {
     /** Whether swipe should be enabled in the given [orientation]. */
     fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
         userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
 
     val currentScene = gestureHandler.currentScene
+    val orientation = gestureHandler.orientation
     val canSwipe = currentScene.shouldEnableSwipes(orientation)
     val canOppositeSwipe =
         currentScene.shouldEnableSwipes(
@@ -84,7 +61,7 @@
 }
 
 @Composable
-private fun rememberSceneGestureHandler(
+internal fun rememberSceneGestureHandler(
     layoutImpl: SceneTransitionLayoutImpl,
     orientation: Orientation,
 ): SceneGestureHandler {
@@ -101,641 +78,3 @@
 
     return gestureHandler
 }
-
-@VisibleForTesting
-class SceneGestureHandler(
-    private val layoutImpl: SceneTransitionLayoutImpl,
-    internal val orientation: Orientation,
-    private val coroutineScope: CoroutineScope,
-) : GestureHandler {
-    override val draggable: DraggableHandler = SceneDraggableHandler(this)
-
-    override val nestedScroll: SceneNestedScrollHandler = SceneNestedScrollHandler(this)
-
-    private var transitionState
-        get() = layoutImpl.state.transitionState
-        set(value) {
-            layoutImpl.state.transitionState = value
-        }
-
-    /**
-     * The transition controlled by this gesture handler. It will be set as the [transitionState] in
-     * the [SceneTransitionLayoutImpl] whenever this handler is driving the current transition.
-     *
-     * Note: the initialScene here does not matter, it's only used for initializing the transition
-     * and will be replaced when a drag event starts.
-     */
-    private val swipeTransition = SwipeTransition(initialScene = currentScene)
-
-    internal val currentScene: Scene
-        get() = layoutImpl.scene(transitionState.currentScene)
-
-    @VisibleForTesting
-    val isDrivingTransition
-        get() = transitionState == swipeTransition
-
-    @VisibleForTesting
-    var isAnimatingOffset
-        get() = swipeTransition.isAnimatingOffset
-        private set(value) {
-            swipeTransition.isAnimatingOffset = value
-        }
-
-    internal val swipeTransitionToScene
-        get() = swipeTransition._toScene
-
-    /**
-     * The velocity threshold at which the intent of the user is to swipe up or down. It is the same
-     * as SwipeableV2Defaults.VelocityThreshold.
-     */
-    @VisibleForTesting val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
-
-    /**
-     * The positional threshold at which the intent of the user is to swipe to the next scene. It is
-     * the same as SwipeableV2Defaults.PositionalThreshold.
-     */
-    private val positionalThreshold = with(layoutImpl.density) { 56.dp.toPx() }
-
-    internal var gestureWithPriority: Any? = null
-
-    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
-        if (isDrivingTransition) {
-            // This [transition] was already driving the animation: simply take over it.
-            // Stop animating and start from where the current offset.
-            swipeTransition.stopOffsetAnimation()
-            return
-        }
-
-        val transition = transitionState
-        if (transition is TransitionState.Transition) {
-            // TODO(b/290184746): Better handle interruptions here if state != idle.
-            Log.w(
-                TAG,
-                "start from TransitionState.Transition is not fully supported: from" +
-                    " ${transition.fromScene} to ${transition.toScene} " +
-                    "(progress ${transition.progress})"
-            )
-        }
-
-        val fromScene = currentScene
-
-        swipeTransition._currentScene = fromScene
-        swipeTransition._fromScene = fromScene
-
-        // We don't know where we are transitioning to yet given that the drag just started, so set
-        // it to fromScene, which will effectively be treated the same as Idle(fromScene).
-        swipeTransition._toScene = fromScene
-
-        swipeTransition.stopOffsetAnimation()
-        swipeTransition.dragOffset = 0f
-
-        // Use the layout size in the swipe orientation for swipe distance.
-        // TODO(b/290184746): Also handle custom distances for transitions. With smaller distances,
-        // we will also have to make sure that we correctly handle overscroll.
-        swipeTransition.absoluteDistance =
-            when (orientation) {
-                Orientation.Horizontal -> layoutImpl.size.width
-                Orientation.Vertical -> layoutImpl.size.height
-            }.toFloat()
-
-        val fromEdge =
-            startedPosition?.let { position ->
-                layoutImpl.edgeDetector.edge(
-                    layoutImpl.size,
-                    position.round(),
-                    layoutImpl.density,
-                    orientation,
-                )
-            }
-
-        swipeTransition.actionUpOrLeft =
-            Swipe(
-                direction =
-                    when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Left
-                        Orientation.Vertical -> SwipeDirection.Up
-                    },
-                pointerCount = pointersDown,
-                fromEdge = fromEdge,
-            )
-
-        swipeTransition.actionDownOrRight =
-            Swipe(
-                direction =
-                    when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Right
-                        Orientation.Vertical -> SwipeDirection.Down
-                    },
-                pointerCount = pointersDown,
-                fromEdge = fromEdge,
-            )
-
-        if (fromEdge == null) {
-            swipeTransition.actionUpOrLeftNoEdge = null
-            swipeTransition.actionDownOrRightNoEdge = null
-        } else {
-            swipeTransition.actionUpOrLeftNoEdge =
-                (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
-            swipeTransition.actionDownOrRightNoEdge =
-                (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
-        }
-
-        if (swipeTransition.absoluteDistance > 0f) {
-            transitionState = swipeTransition
-        }
-    }
-
-    internal fun onDrag(delta: Float) {
-        if (delta == 0f) return
-
-        swipeTransition.dragOffset += delta
-
-        // First check transition.fromScene should be changed for the case where the user quickly
-        // swiped twice in a row to accelerate the transition and go from A => B then B => C really
-        // fast.
-        maybeHandleAcceleratedSwipe()
-
-        val offset = swipeTransition.dragOffset
-        val fromScene = swipeTransition._fromScene
-
-        // Compute the target scene depending on the current offset.
-        val target = fromScene.findTargetSceneAndDistance(offset)
-
-        if (swipeTransition._toScene.key != target.sceneKey) {
-            swipeTransition._toScene = layoutImpl.scenes.getValue(target.sceneKey)
-        }
-
-        if (swipeTransition._distance != target.distance) {
-            swipeTransition._distance = target.distance
-        }
-    }
-
-    /**
-     * Change fromScene in the case where the user quickly swiped multiple times in the same
-     * direction to accelerate the transition from A => B then B => C.
-     */
-    private fun maybeHandleAcceleratedSwipe() {
-        val toScene = swipeTransition._toScene
-        val fromScene = swipeTransition._fromScene
-
-        // If the swipe was not committed, don't do anything.
-        if (fromScene == toScene || swipeTransition._currentScene != toScene) {
-            return
-        }
-
-        // If the offset is past the distance then let's change fromScene so that the user can swipe
-        // to the next screen or go back to the previous one.
-        val offset = swipeTransition.dragOffset
-        val absoluteDistance = swipeTransition.absoluteDistance
-        if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
-            swipeTransition.dragOffset += absoluteDistance
-            swipeTransition._fromScene = toScene
-        } else if (
-            offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
-        ) {
-            swipeTransition.dragOffset -= absoluteDistance
-            swipeTransition._fromScene = toScene
-        }
-
-        // Important note: toScene and distance will be updated right after this function is called,
-        // using fromScene and dragOffset.
-    }
-
-    private class TargetScene(
-        val sceneKey: SceneKey,
-        val distance: Float,
-    )
-
-    private fun Scene.findTargetSceneAndDistance(directionOffset: Float): TargetScene {
-        val upOrLeft = swipeTransition.upOrLeft(this)
-        val downOrRight = swipeTransition.downOrRight(this)
-
-        // Compute the target scene depending on the current offset.
-        return when {
-            directionOffset < 0f && upOrLeft != null -> {
-                TargetScene(
-                    sceneKey = upOrLeft,
-                    distance = -swipeTransition.absoluteDistance,
-                )
-            }
-            directionOffset > 0f && downOrRight != null -> {
-                TargetScene(
-                    sceneKey = downOrRight,
-                    distance = swipeTransition.absoluteDistance,
-                )
-            }
-            else -> {
-                TargetScene(
-                    sceneKey = key,
-                    distance = 0f,
-                )
-            }
-        }
-    }
-
-    internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
-        // The state was changed since the drag started; don't do anything.
-        if (!isDrivingTransition) {
-            return
-        }
-
-        fun animateTo(targetScene: Scene, targetOffset: Float) {
-            // If the effective current scene changed, it should be reflected right now in the
-            // current scene state, even before the settle animation is ongoing. That way all the
-            // swipeables and back handlers will be refreshed and the user can for instance quickly
-            // swipe vertically from A => B then horizontally from B => C, or swipe from A => B then
-            // immediately go back B => A.
-            if (targetScene != swipeTransition._currentScene) {
-                swipeTransition._currentScene = targetScene
-                layoutImpl.onChangeScene(targetScene.key)
-            }
-
-            animateOffset(
-                initialVelocity = velocity,
-                targetOffset = targetOffset,
-                targetScene = targetScene.key
-            )
-        }
-
-        val fromScene = swipeTransition._fromScene
-        if (canChangeScene) {
-            // If we are halfway between two scenes, we check what the target will be based on the
-            // velocity and offset of the transition, then we launch the animation.
-
-            val toScene = swipeTransition._toScene
-            if (fromScene == toScene) {
-                // We were not animating.
-                transitionState = TransitionState.Idle(fromScene.key)
-                return
-            }
-
-            // Compute the destination scene (and therefore offset) to settle in.
-            val offset = swipeTransition.dragOffset
-            val distance = swipeTransition.distance
-            if (
-                shouldCommitSwipe(
-                    offset,
-                    distance,
-                    velocity,
-                    wasCommitted = swipeTransition._currentScene == toScene,
-                )
-            ) {
-                // Animate to the next scene
-                animateTo(targetScene = toScene, targetOffset = distance)
-            } else {
-                // Animate to the initial scene
-                animateTo(targetScene = fromScene, targetOffset = 0f)
-            }
-        } else {
-            // We are doing an overscroll animation between scenes. In this case, we can also start
-            // from the idle position.
-
-            val startFromIdlePosition = swipeTransition.dragOffset == 0f
-
-            if (startFromIdlePosition) {
-                // If there is a next scene, we start the overscroll animation.
-                val target = fromScene.findTargetSceneAndDistance(velocity)
-                val isValidTarget = target.distance != 0f && target.sceneKey != fromScene.key
-                if (isValidTarget) {
-                    swipeTransition._toScene = layoutImpl.scene(target.sceneKey)
-                    swipeTransition._distance = target.distance
-
-                    animateTo(targetScene = fromScene, targetOffset = 0f)
-                } else {
-                    // We will not animate
-                    transitionState = TransitionState.Idle(fromScene.key)
-                }
-            } else {
-                // We were between two scenes: animate to the initial scene.
-                animateTo(targetScene = fromScene, targetOffset = 0f)
-            }
-        }
-    }
-
-    /**
-     * Whether the swipe to the target scene should be committed or not. This is inspired by
-     * SwipeableV2.computeTarget().
-     */
-    private fun shouldCommitSwipe(
-        offset: Float,
-        distance: Float,
-        velocity: Float,
-        wasCommitted: Boolean,
-    ): Boolean {
-        fun isCloserToTarget(): Boolean {
-            return (offset - distance).absoluteValue < offset.absoluteValue
-        }
-
-        // Swiping up or left.
-        if (distance < 0f) {
-            return if (offset > 0f || velocity >= velocityThreshold) {
-                false
-            } else {
-                velocity <= -velocityThreshold ||
-                    (offset <= -positionalThreshold && !wasCommitted) ||
-                    isCloserToTarget()
-            }
-        }
-
-        // Swiping down or right.
-        return if (offset < 0f || velocity <= -velocityThreshold) {
-            false
-        } else {
-            velocity >= velocityThreshold ||
-                (offset >= positionalThreshold && !wasCommitted) ||
-                isCloserToTarget()
-        }
-    }
-
-    private fun animateOffset(
-        initialVelocity: Float,
-        targetOffset: Float,
-        targetScene: SceneKey,
-    ) {
-        swipeTransition.startOffsetAnimation {
-            coroutineScope.launch {
-                if (!isAnimatingOffset) {
-                    swipeTransition.offsetAnimatable.snapTo(swipeTransition.dragOffset)
-                }
-                isAnimatingOffset = true
-
-                swipeTransition.offsetAnimatable.animateTo(
-                    targetOffset,
-                    // TODO(b/290184746): Make this spring spec configurable.
-                    spring(
-                        stiffness = Spring.StiffnessMediumLow,
-                        visibilityThreshold = OffsetVisibilityThreshold
-                    ),
-                    initialVelocity = initialVelocity,
-                )
-
-                isAnimatingOffset = false
-
-                // Now that the animation is done, the state should be idle. Note that if the state
-                // was changed since this animation started, some external code changed it and we
-                // shouldn't do anything here. Note also that this job will be cancelled in the case
-                // where the user intercepts this swipe.
-                if (isDrivingTransition) {
-                    transitionState = TransitionState.Idle(targetScene)
-                }
-            }
-        }
-    }
-
-    private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
-        var _currentScene by mutableStateOf(initialScene)
-        override val currentScene: SceneKey
-            get() = _currentScene.key
-
-        var _fromScene by mutableStateOf(initialScene)
-        override val fromScene: SceneKey
-            get() = _fromScene.key
-
-        var _toScene by mutableStateOf(initialScene)
-        override val toScene: SceneKey
-            get() = _toScene.key
-
-        override val progress: Float
-            get() {
-                val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
-                if (distance == 0f) {
-                    // This can happen only if fromScene == toScene.
-                    error(
-                        "Transition.progress should be called only when Transition.fromScene != " +
-                            "Transition.toScene"
-                    )
-                }
-                return offset / distance
-            }
-
-        override val isInitiatedByUserInput = true
-
-        /** The current offset caused by the drag gesture. */
-        var dragOffset by mutableFloatStateOf(0f)
-
-        /**
-         * Whether the offset is animated (the user lifted their finger) or if it is driven by
-         * gesture.
-         */
-        var isAnimatingOffset by mutableStateOf(false)
-
-        // If we are not animating offset, it means the offset is being driven by the user's finger.
-        override val isUserInputOngoing: Boolean
-            get() = !isAnimatingOffset
-
-        /** The animatable used to animate the offset once the user lifted its finger. */
-        val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
-
-        /** Job to check that there is at most one offset animation in progress. */
-        private var offsetAnimationJob: Job? = null
-
-        /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
-        fun startOffsetAnimation(job: () -> Job) {
-            stopOffsetAnimation()
-            offsetAnimationJob = job()
-        }
-
-        /** Stops any ongoing offset animation. */
-        fun stopOffsetAnimation() {
-            offsetAnimationJob?.cancel()
-
-            if (isAnimatingOffset) {
-                isAnimatingOffset = false
-                dragOffset = offsetAnimatable.value
-            }
-        }
-
-        /** The absolute distance between [fromScene] and [toScene]. */
-        var absoluteDistance = 0f
-
-        /**
-         * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
-         * above or to the left of [toScene].
-         */
-        var _distance by mutableFloatStateOf(0f)
-        val distance: Float
-            get() = _distance
-
-        /** The [UserAction]s associated to this swipe. */
-        var actionUpOrLeft: UserAction = Back
-        var actionDownOrRight: UserAction = Back
-        var actionUpOrLeftNoEdge: UserAction? = null
-        var actionDownOrRightNoEdge: UserAction? = null
-
-        fun upOrLeft(scene: Scene): SceneKey? {
-            return scene.userActions[actionUpOrLeft]
-                ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
-        }
-
-        fun downOrRight(scene: Scene): SceneKey? {
-            return scene.userActions[actionDownOrRight]
-                ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
-        }
-    }
-
-    companion object {
-        private const val TAG = "SceneGestureHandler"
-    }
-}
-
-private class SceneDraggableHandler(
-    private val gestureHandler: SceneGestureHandler,
-) : DraggableHandler {
-    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
-        gestureHandler.gestureWithPriority = this
-        gestureHandler.onDragStarted(pointersDown, startedPosition)
-    }
-
-    override fun onDelta(pixels: Float) {
-        if (gestureHandler.gestureWithPriority == this) {
-            gestureHandler.onDrag(delta = pixels)
-        }
-    }
-
-    override fun onDragStopped(velocity: Float) {
-        if (gestureHandler.gestureWithPriority == this) {
-            gestureHandler.gestureWithPriority = null
-            gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
-        }
-    }
-}
-
-@VisibleForTesting
-class SceneNestedScrollHandler(
-    private val gestureHandler: SceneGestureHandler,
-) : NestedScrollHandler {
-    override val connection: PriorityNestedScrollConnection = nestedScrollConnection()
-
-    private fun Offset.toAmount() =
-        when (gestureHandler.orientation) {
-            Orientation.Horizontal -> x
-            Orientation.Vertical -> y
-        }
-
-    private fun Velocity.toAmount() =
-        when (gestureHandler.orientation) {
-            Orientation.Horizontal -> x
-            Orientation.Vertical -> y
-        }
-
-    private fun Float.toOffset() =
-        when (gestureHandler.orientation) {
-            Orientation.Horizontal -> Offset(x = this, y = 0f)
-            Orientation.Vertical -> Offset(x = 0f, y = this)
-        }
-
-    private fun nestedScrollConnection(): PriorityNestedScrollConnection {
-        // The next potential scene is calculated during the canStart
-        var nextScene: SceneKey? = null
-
-        // This is the scene on which we will have priority during the scroll gesture.
-        var priorityScene: SceneKey? = null
-
-        // If we performed a long gesture before entering priority mode, we would have to avoid
-        // moving on to the next scene.
-        var gestureStartedOnNestedChild = false
-
-        val actionUpOrLeft =
-            Swipe(
-                direction =
-                    when (gestureHandler.orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Left
-                        Orientation.Vertical -> SwipeDirection.Up
-                    },
-                pointerCount = 1,
-            )
-
-        val actionDownOrRight =
-            Swipe(
-                direction =
-                    when (gestureHandler.orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Right
-                        Orientation.Vertical -> SwipeDirection.Down
-                    },
-                pointerCount = 1,
-            )
-
-        fun findNextScene(amount: Float): SceneKey? {
-            val fromScene = gestureHandler.currentScene
-            return when {
-                amount < 0f -> fromScene.userActions[actionUpOrLeft]
-                amount > 0f -> fromScene.userActions[actionDownOrRight]
-                else -> null
-            }
-        }
-
-        return PriorityNestedScrollConnection(
-            canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
-                gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero
-
-                val canInterceptPreScroll =
-                    gestureHandler.isDrivingTransition &&
-                        !gestureStartedOnNestedChild &&
-                        offsetAvailable.toAmount() != 0f
-
-                if (!canInterceptPreScroll) return@PriorityNestedScrollConnection false
-
-                nextScene = gestureHandler.swipeTransitionToScene.key
-
-                true
-            },
-            canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
-                val amount = offsetAvailable.toAmount()
-                if (amount == 0f) return@PriorityNestedScrollConnection false
-
-                gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero
-                nextScene = findNextScene(amount)
-                nextScene != null
-            },
-            canStartPostFling = { velocityAvailable ->
-                val amount = velocityAvailable.toAmount()
-                if (amount == 0f) return@PriorityNestedScrollConnection false
-
-                // We could start an overscroll animation
-                gestureStartedOnNestedChild = true
-                nextScene = findNextScene(amount)
-                nextScene != null
-            },
-            canContinueScroll = { priorityScene == gestureHandler.swipeTransitionToScene.key },
-            onStart = {
-                gestureHandler.gestureWithPriority = this
-                priorityScene = nextScene
-                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
-            },
-            onScroll = { offsetAvailable ->
-                if (gestureHandler.gestureWithPriority != this) {
-                    return@PriorityNestedScrollConnection Offset.Zero
-                }
-
-                val amount = offsetAvailable.toAmount()
-
-                // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
-                // initiated in a nested child.
-                gestureHandler.onDrag(amount)
-
-                amount.toOffset()
-            },
-            onStop = { velocityAvailable ->
-                if (gestureHandler.gestureWithPriority != this) {
-                    return@PriorityNestedScrollConnection Velocity.Zero
-                }
-
-                priorityScene = null
-
-                gestureHandler.onDragStopped(
-                    velocity = velocityAvailable.toAmount(),
-                    canChangeScene = !gestureStartedOnNestedChild
-                )
-
-                // The onDragStopped animation consumes any remaining velocity.
-                velocityAvailable
-            },
-        )
-    }
-}
-
-/**
- * The number of pixels below which there won't be a visible difference in the transition and from
- * which the animation can stop.
- */
-private const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index 7b7695e..5473186 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -66,7 +66,8 @@
             val int by animateSharedIntAsState(targetValues.int, TestValues.Value1, key)
             val float by animateSharedFloatAsState(targetValues.float, TestValues.Value2, key)
             val dp by animateSharedDpAsState(targetValues.dp, TestValues.Value3, key)
-            val color by animateSharedColorAsState(targetValues.color, TestValues.Value4, key)
+            val color by
+                animateSharedColorAsState(targetValues.color, TestValues.Value4, element = null)
 
             // Make sure we read the values during composition, so that we recompose and call
             // onCurrentValueChanged() with the latest values.
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
new file mode 100644
index 0000000..de46f72
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+
+/** `SceneScope` for tests, which allows a single scene to be drawn in a [SceneTransitionLayout]. */
+@Composable
+fun TestSceneScope(
+    modifier: Modifier = Modifier,
+    content: @Composable SceneScope.() -> Unit,
+) {
+    val currentScene = remember { SceneKey("current") }
+    SceneTransitionLayout(
+        currentScene,
+        onChangeScene = { /* do nothing */},
+        transitions = remember { transitions {} },
+        modifier,
+    ) {
+        scene(currentScene, content = content)
+    }
+}
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 1beb55b..0537f17 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -26,12 +26,19 @@
     name: "SystemUIPluginLib",
 
     srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
         "bcsmartspace/src/**/*.java",
         "bcsmartspace/src/**/*.kt",
+        "src/**/*.java",
+        "src/**/*.kt",
     ],
 
+    optimize: {
+        proguard_flags_files: [
+            "proguard_plugins.flags",
+        ],
+        export_proguard_flags_files: true,
+    },
+
     // If you add a static lib here, you may need to also add the package to the ClassLoaderFilter
     // in PluginInstance. That will ensure that loaded plugins have access to the related classes.
     // You should also add it to proguard_common.flags so that proguard does not remove the portions
@@ -43,6 +50,7 @@
         "SystemUIAnimationLib",
         "SystemUICommon",
         "SystemUILogLib",
+        "androidx.annotation_annotation",
     ],
 
 }
diff --git a/packages/SystemUI/plugin/proguard_plugins.flags b/packages/SystemUI/plugin/proguard_plugins.flags
new file mode 100644
index 0000000..abac27f
--- /dev/null
+++ b/packages/SystemUI/plugin/proguard_plugins.flags
@@ -0,0 +1,9 @@
+# The plugins and core log subpackages act as shared libraries that might be referenced in
+# dynamically-loaded plugin APKs.
+-keep class com.android.systemui.plugins.** {
+    *;
+}
+
+-keep class com.android.systemui.log.core.** {
+    *;
+}
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index b534fcec..42b5923 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -4,4 +4,4 @@
     *;
 }
 
--keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; }
\ No newline at end of file
+-keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; }
diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags
index 445bdc2..21b019e 100644
--- a/packages/SystemUI/proguard_common.flags
+++ b/packages/SystemUI/proguard_common.flags
@@ -1,3 +1,4 @@
+-include proguard_kotlin.flags
 -keep class com.android.systemui.VendorServices
 
 # Needed to ensure callback field references are kept in their respective
@@ -20,16 +21,6 @@
     public <init>(android.content.Context, android.util.AttributeSet);
 }
 
--keep class com.android.systemui.tuner.*
-
-# The plugins and core log subpackages act as shared libraries that might be referenced in
-# dynamically-loaded plugin APKs.
--keep class com.android.systemui.plugins.** {
-    *;
-}
--keep class com.android.systemui.log.core.** {
-    *;
-}
 -keep class androidx.core.app.CoreComponentFactory
 
 # Keep the wm shell lib
@@ -51,45 +42,6 @@
 # part of optimization. This lets proguard inline trivial getter/setter methods.
 -allowaccessmodification
 
-# Removes runtime checks added through Kotlin to JVM code genereration to
-# avoid linear growth as more Kotlin code is converted / added to the codebase.
-# These checks are generally applied to Java platform types (values returned
-# from Java code that don't have nullness annotations), but we remove them to
-# avoid code size increases.
-#
-# See also https://kotlinlang.org/docs/reference/java-interop.html
-#
-# TODO(b/199941987): Consider standardizing these rules in a central place as
-# Kotlin gains adoption with other platform targets.
--assumenosideeffects class kotlin.jvm.internal.Intrinsics {
-    # Remove check for method parameters being null
-    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
-
-    # When a Java platform type is returned and passed to Kotlin NonNull method,
-    # remove the null check
-    static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
-    static void checkNotNullExpressionValue(java.lang.Object, java.lang.String);
-
-    # Remove check that final value returned from method is null, if passing
-    # back Java platform type.
-    static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
-    static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String);
-
-    # Null check for accessing a field from a parent class written in Java.
-    static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
-    static void checkFieldIsNotNull(java.lang.Object, java.lang.String);
-
-    # Removes code generated from !! operator which converts Nullable type to
-    # NonNull type. These would throw an NPE immediate after on access.
-    static void checkNotNull(java.lang.Object, java.lang.String);
-    static void checkNotNullParameter(java.lang.Object, java.lang.String);
-
-    # Removes lateinit var check being used before being set. Check is applied
-    # on every field access without this.
-    static void throwUninitializedPropertyAccessException(java.lang.String);
-}
-
-
 # Strip verbose logs.
 -assumenosideeffects class android.util.Log {
   static *** v(...);
diff --git a/packages/SystemUI/proguard_kotlin.flags b/packages/SystemUI/proguard_kotlin.flags
new file mode 100644
index 0000000..ceea3c8
--- /dev/null
+++ b/packages/SystemUI/proguard_kotlin.flags
@@ -0,0 +1,37 @@
+# Removes runtime checks added through Kotlin to JVM code genereration to
+# avoid linear growth as more Kotlin code is converted / added to the codebase.
+# These checks are generally applied to Java platform types (values returned
+# from Java code that don't have nullness annotations), but we remove them to
+# avoid code size increases.
+#
+# See also https://kotlinlang.org/docs/reference/java-interop.html
+#
+# TODO(b/199941987): Consider standardizing these rules in a central place as
+# Kotlin gains adoption with other platform targets.
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+    # Remove check for method parameters being null
+    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
+
+    # When a Java platform type is returned and passed to Kotlin NonNull method,
+    # remove the null check
+    static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
+    static void checkNotNullExpressionValue(java.lang.Object, java.lang.String);
+
+    # Remove check that final value returned from method is null, if passing
+    # back Java platform type.
+    static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
+    static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String);
+
+    # Null check for accessing a field from a parent class written in Java.
+    static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
+    static void checkFieldIsNotNull(java.lang.Object, java.lang.String);
+
+    # Removes code generated from !! operator which converts Nullable type to
+    # NonNull type. These would throw an NPE immediate after on access.
+    static void checkNotNull(java.lang.Object, java.lang.String);
+    static void checkNotNullParameter(java.lang.Object, java.lang.String);
+
+    # Removes lateinit var check being used before being set. Check is applied
+    # on every field access without this.
+    static void throwUninitializedPropertyAccessException(java.lang.String);
+}
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 1a37e2d..73ee50d 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -736,6 +736,8 @@
     <!-- Whether the communal service should be enabled -->
     <bool name="config_communalServiceEnabled">false</bool>
 
+    <!-- Name of the database that stores info of widgets shown on glanceable hub -->
+    <string name="config_communalDatabase" translatable="false">communal_db</string>
     <!-- Component names of allowed communal widgets -->
     <string-array name="config_communalWidgetAllowlist" translatable="false" />
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 12bff4a..9a3c6d5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1048,6 +1048,11 @@
     <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
     <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>
 
+    <!-- Description for the button that opens the widget picker on click. [CHAR LIMIT=50] -->
+    <string name="button_to_open_widget_picker">Open the widget picker</string>
+    <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
+    <string name="button_to_remove_widget">Remove a widget</string>
+
     <!-- Related to user switcher --><skip/>
 
     <!-- Accessibility label for the button that opens the user switcher. -->
@@ -3261,4 +3266,9 @@
     <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
     <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
     <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+
+    <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
+    <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
+    <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
+    <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
 </resources>
diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/1.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/1.json
new file mode 100644
index 0000000..ffc4d91
--- /dev/null
+++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/1.json
@@ -0,0 +1,79 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "38f223811a414587ee1b6445ae19385d",
+    "entities": [
+      {
+        "tableName": "communal_widget_table",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "uid",
+            "columnName": "uid",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "widgetId",
+            "columnName": "widget_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "componentName",
+            "columnName": "component_name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "itemId",
+            "columnName": "item_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "uid"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "communal_item_rank_table",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "uid",
+            "columnName": "uid",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "rank",
+            "columnName": "rank",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "uid"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38f223811a414587ee1b6445ae19385d')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
index a14f971..f005af3 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
@@ -28,6 +28,7 @@
 import android.graphics.RecordingCanvas;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Trace;
 import android.view.RenderNodeAnimator;
 import android.view.View;
@@ -48,6 +49,7 @@
     private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
     private static final int ANIMATION_DURATION_SCALE = 350;
     private static final int ANIMATION_DURATION_FADE = 450;
+    private static final int ANIMATION_DURATION_FADE_FAST = 80;
     private static final Interpolator ALPHA_OUT_INTERPOLATOR =
             new PathInterpolator(0f, 0f, 0.8f, 1f);
 
@@ -71,6 +73,9 @@
     private boolean mLastDark;
     private boolean mDark;
     private boolean mDelayTouchFeedback;
+    private boolean mSpeedUpNextFade;
+    // When non-null, this runs the next time this ripple is drawn invisibly.
+    private Runnable mOnInvisibleRunnable;
 
     private final Interpolator mInterpolator = new LogInterpolator();
     private boolean mSupportHardware;
@@ -112,6 +117,18 @@
         mDelayTouchFeedback = delay;
     }
 
+    /** Next time we fade out (pressed==false), use a shorter duration than the standard. */
+    public void speedUpNextFade() {
+        mSpeedUpNextFade = true;
+    }
+
+    /**
+     *  @param onInvisibleRunnable run after we are next drawn invisibly. Only used once.
+     */
+    void setOnInvisibleRunnable(Runnable onInvisibleRunnable) {
+        mOnInvisibleRunnable = onInvisibleRunnable;
+    }
+
     public void setType(Type type) {
         mType = type;
     }
@@ -161,6 +178,11 @@
         } else {
             drawSoftware(canvas);
         }
+
+        if (!mPressed && !mVisible && mOnInvisibleRunnable != null) {
+            new Handler(Looper.getMainLooper()).post(mOnInvisibleRunnable);
+            mOnInvisibleRunnable = null;
+        }
     }
 
     @Override
@@ -270,7 +292,7 @@
         return true;
     }
 
-    public void setPressed(boolean pressed) {
+    private void setPressed(boolean pressed) {
         if (mDark != mLastDark && pressed) {
             mRipplePaint = null;
             mLastDark = mDark;
@@ -350,7 +372,7 @@
     private void exitSoftware() {
         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
         alphaAnimator.setInterpolator(ALPHA_OUT_INTERPOLATOR);
-        alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
+        alphaAnimator.setDuration(getFadeDuration());
         alphaAnimator.addListener(mAnimatorListener);
         alphaAnimator.start();
         mRunningAnimations.add(alphaAnimator);
@@ -414,6 +436,12 @@
         return Math.min(size, mMaxWidth);
     }
 
+    private int getFadeDuration() {
+        int duration = mSpeedUpNextFade ? ANIMATION_DURATION_FADE_FAST : ANIMATION_DURATION_FADE;
+        mSpeedUpNextFade = false;
+        return duration;
+    }
+
     private void enterHardware() {
         endAnimations("enterHardware", true /* cancel */);
         mVisible = true;
@@ -471,7 +499,7 @@
         mPaintProp = CanvasProperty.createPaint(getRipplePaint());
         final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
                 RenderNodeAnimator.PAINT_ALPHA, 0);
-        opacityAnim.setDuration(ANIMATION_DURATION_FADE);
+        opacityAnim.setDuration(getFadeDuration());
         opacityAnim.setInterpolator(ALPHA_OUT_INTERPOLATOR);
         opacityAnim.addListener(mAnimatorListener);
         opacityAnim.addListener(mExitHwTraceAnimator);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index b44bf39..fec96c6 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.shared.rotation;
 
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
+
 import android.annotation.DimenRes;
 import android.annotation.IdRes;
 import android.annotation.LayoutRes;
@@ -87,15 +89,15 @@
             @DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin,
             @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
             @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
-        mWindowManager = context.getSystemService(WindowManager.class);
-        mKeyButtonContainer = (ViewGroup) LayoutInflater.from(context).inflate(layout, null);
+        mContext = context.createWindowContext(context.getDisplay(), TYPE_NAVIGATION_BAR_PANEL,
+                null);
+        mWindowManager = mContext.getSystemService(WindowManager.class);
+        mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null);
         mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
         mKeyButtonView.setVisibility(View.VISIBLE);
-        mKeyButtonView.setContentDescription(context.getString(contentDescriptionResource));
+        mKeyButtonView.setContentDescription(mContext.getString(contentDescriptionResource));
         mKeyButtonView.setRipple(rippleMaxWidth);
 
-        mContext = context;
-
         mContentDescriptionResource = contentDescriptionResource;
         mMinMarginResource = minMargin;
         mRoundedContentPaddingResource = roundedContentPadding;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index f9eb686..6b8009d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -47,6 +47,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder;
+import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.LogLevel;
@@ -123,6 +124,7 @@
     private View mSmartspaceView;
 
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    private final InWindowLauncherUnlockAnimationManager mInWindowLauncherUnlockAnimationManager;
 
     private boolean mShownOnSecondaryDisplay = false;
     private boolean mOnlyClock = false;
@@ -190,7 +192,8 @@
             AlwaysOnDisplayNotificationIconViewStore aodIconViewStore,
             KeyguardInteractor keyguardInteractor,
             KeyguardClockInteractor keyguardClockInteractor,
-            FeatureFlagsClassic featureFlags) {
+            FeatureFlagsClassic featureFlags,
+            InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mClockRegistry = clockRegistry;
@@ -214,6 +217,7 @@
         mFeatureFlags = featureFlags;
         mKeyguardInteractor = keyguardInteractor;
         mKeyguardClockInteractor = keyguardClockInteractor;
+        mInWindowLauncherUnlockAnimationManager = inWindowLauncherUnlockAnimationManager;
 
         mClockChangedListener = new ClockRegistry.ClockChangeListener() {
             @Override
@@ -438,6 +442,8 @@
         mSmartspaceView.setPaddingRelative(startPadding, 0, endPadding, 0);
 
         mKeyguardUnlockAnimationController.setLockscreenSmartspace(mSmartspaceView);
+        mInWindowLauncherUnlockAnimationManager.setLockscreenSmartspace(mSmartspaceView);
+
         mView.setSmartspace(mSmartspaceView);
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 181aa5c..f091558 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -22,6 +22,7 @@
 import static com.android.keyguard.LockIconView.ICON_FINGERPRINT;
 import static com.android.keyguard.LockIconView.ICON_LOCK;
 import static com.android.keyguard.LockIconView.ICON_UNLOCK;
+import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
 import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
@@ -459,7 +460,7 @@
     private void updateLockIconLocation() {
         final float scaleFactor = mAuthController.getScaleFactor();
         final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor);
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
+        if (keyguardBottomAreaRefactor()) {
             mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding,
                     scaledPadding);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING
index 055fad1..be26b43 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING
@@ -5,6 +5,18 @@
       "options": [
         {
           "include-filter": "com.android.systemui.accessibility"
+        },
+        {
+            "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+            "exclude-annotation": "android.platform.test.annotations.Postsubmit"
+        },
+        {
+            "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+        },
+        {
+            "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IRadiiAnimationListener.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IRadiiAnimationListener.java
index 72935f7..d92b506 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IRadiiAnimationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/IRadiiAnimationListener.java
@@ -18,4 +18,8 @@
 
 interface IRadiiAnimationListener {
     void onRadiiAnimationUpdate(float[] radii);
+
+    void onRadiiAnimationStart();
+
+    void onRadiiAnimationStop();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index 761551c..34d7cec 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -94,7 +94,19 @@
         mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
         mFadeOutAnimator.addUpdateListener(
                 (animation) -> menuView.setAlpha((float) animation.getAnimatedValue()));
-        mRadiiAnimator = new RadiiAnimator(mMenuViewAppearance.getMenuRadii(), mMenuView::setRadii);
+        mRadiiAnimator = new RadiiAnimator(mMenuViewAppearance.getMenuRadii(),
+                new IRadiiAnimationListener() {
+                    @Override
+                    public void onRadiiAnimationUpdate(float[] radii) {
+                        mMenuView.setRadii(radii);
+                    }
+
+                    @Override
+                    public void onRadiiAnimationStart() {}
+
+                    @Override
+                    public void onRadiiAnimationStop() {}
+                });
     }
 
     void moveToPosition(PointF position) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimator.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimator.java
index acad36e..4aa0d89 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimator.java
@@ -55,15 +55,19 @@
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
                 animationListener.onRadiiAnimationUpdate(evaluate(/* t = */ 0.0f));
+                animationListener.onRadiiAnimationStart();
             }
 
             @Override
-            public void onAnimationEnd(@NonNull Animator animation) {}
+            public void onAnimationEnd(@NonNull Animator animation) {
+                animationListener.onRadiiAnimationStop();
+            }
 
             @Override
             public void onAnimationCancel(@NonNull Animator animation) {
                 animationListener.onRadiiAnimationUpdate(
                         evaluate(mAnimationDriver.getAnimatedFraction()));
+                animationListener.onRadiiAnimationStop();
             }
 
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FpsUnlockTracker.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FpsUnlockTracker.kt
new file mode 100644
index 0000000..cf699a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FpsUnlockTracker.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+import android.hardware.biometrics.BiometricSourceType
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import android.util.Log
+import com.android.app.tracing.TraceStateLogger
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import javax.inject.Inject
+
+private const val TAG = "FpsUnlockTracker"
+private const val TRACE_COUNTER_NAME = "FpsUnlockStage"
+private const val TRACE_TAG_AOD = "AOD"
+private const val TRACE_TAG_KEYGUARD = "KEYGUARD"
+private const val DEBUG = true
+
+/** This is a class for monitoring unlock latency of fps and logging stages in perfetto. */
+@SysUISingleton
+class FpsUnlockTracker
+@Inject
+constructor(
+    private val statusBarStateController: StatusBarStateController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
+    private val latencyTracker: LatencyTracker,
+) {
+    private val fpsTraceStateLogger = TraceStateLogger(TRACE_COUNTER_NAME)
+    private var fpsAuthenticated: Boolean = false
+
+    private val keyguardUpdateMonitorCallback =
+        object : KeyguardUpdateMonitorCallback() {
+            override fun onBiometricAcquired(
+                biometricSourceType: BiometricSourceType?,
+                acquireInfo: Int
+            ) {
+                if (keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed) {
+                    onHalAuthenticationStage(acquireInfo)
+                }
+            }
+
+            override fun onBiometricAuthenticated(
+                userId: Int,
+                biometricSourceType: BiometricSourceType?,
+                isStrongBiometric: Boolean
+            ) {
+                if (biometricSourceType == FINGERPRINT) {
+                    fpsAuthenticated = true
+                    onExitKeyguard()
+                }
+            }
+
+            override fun onBiometricError(
+                msgId: Int,
+                errString: String?,
+                biometricSourceType: BiometricSourceType?
+            ) {
+                if (biometricSourceType == FINGERPRINT) {
+                    latencyTracker.onActionCancel(ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME)
+                }
+            }
+
+            override fun onBiometricRunningStateChanged(
+                running: Boolean,
+                biometricSourceType: BiometricSourceType?
+            ) {
+                if (biometricSourceType != FINGERPRINT || !running) {
+                    return
+                }
+                onWaitForAuthenticationStage()
+            }
+        }
+
+    private val keyguardUnlockAnimationListener =
+        object : KeyguardUnlockAnimationListener {
+            override fun onUnlockAnimationFinished() = onUnlockedStage()
+        }
+
+    /** Start tracking the fps unlock. */
+    fun startTracking() {
+        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
+            keyguardUnlockAnimationListener
+        )
+    }
+
+    /** Stop tracking the fps unlock. */
+    fun stopTracking() {
+        keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
+        keyguardUnlockAnimationController.removeKeyguardUnlockAnimationListener(
+            keyguardUnlockAnimationListener
+        )
+    }
+
+    /**
+     * The stage when the devices is locked and is possible to be unlocked via fps. However, in some
+     * situations, it might be unlocked only via bouncer.
+     */
+    fun onWaitForAuthenticationStage() {
+        val stage =
+            if (keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+                FpsUnlockStage.WAIT_FOR_AUTHENTICATION.name
+            else FpsUnlockStage.WAIT_FOR_AUTHENTICATION.name + "(Not allowed)"
+        fpsTraceStateLogger.log(stage)
+        if (DEBUG) {
+            Log.d(TAG, "onWaitForAuthenticationStage: stage=$stage")
+        }
+    }
+
+    /**
+     * The stage dedicated to UDFPS, SFPS should not enter this stage. The only place where invokes
+     * this function is UdfpsController#onFingerDown.
+     */
+    fun onUiReadyStage() {
+        if (!keyguardUpdateMonitor.isUdfpsSupported || !keyguardUpdateMonitor.isUdfpsEnrolled) {
+            return
+        }
+        fpsTraceStateLogger.log(FpsUnlockStage.UI_READY.name)
+        startLatencyTracker()
+        if (DEBUG) {
+            Log.d(TAG, "onUiReadyStage: dozing=${statusBarStateController.isDozing}")
+        }
+    }
+
+    /** The stage when the HAL is authenticating the fingerprint. */
+    fun onHalAuthenticationStage(acquire: Int) {
+        fpsTraceStateLogger.log("${FpsUnlockStage.HAL_AUTHENTICATION.name}($acquire)")
+        // Start latency tracker here only for SFPS, UDFPS should start at onUiReadyStage.
+        if (
+            keyguardUpdateMonitor.isSfpsSupported &&
+                keyguardUpdateMonitor.isSfpsEnrolled &&
+                acquire == FINGERPRINT_ACQUIRED_START
+        ) {
+            startLatencyTracker()
+        }
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "onHalAuthenticationStage: acquire=$acquire" +
+                    ", sfpsSupported=${keyguardUpdateMonitor.isSfpsSupported}" +
+                    ", sfpsEnrolled=${keyguardUpdateMonitor.isSfpsEnrolled}"
+            )
+        }
+    }
+
+    /** The stage when the authentication is succeeded and is going to exit keyguard. */
+    fun onExitKeyguard() {
+        fpsTraceStateLogger.log(FpsUnlockStage.EXIT_KEYGUARD.name)
+        if (DEBUG) {
+            Log.d(TAG, "onExitKeyguard: fpsAuthenticated=$fpsAuthenticated")
+        }
+    }
+
+    /**
+     * The stage when the unlock animation is finished which means the user can start interacting
+     * with the device.
+     */
+    fun onUnlockedStage() {
+        fpsTraceStateLogger.log(FpsUnlockStage.UNLOCKED.name)
+        if (fpsAuthenticated) {
+            // The device is unlocked successfully via fps, end the instrument.
+            latencyTracker.onActionEnd(ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME)
+        } else {
+            // The device is unlocked but not via fps, maybe bouncer? Cancel the instrument.
+            latencyTracker.onActionCancel(ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME)
+        }
+        if (DEBUG) {
+            Log.d(TAG, "onUnlockedStage: fpsAuthenticated=$fpsAuthenticated")
+        }
+        fpsAuthenticated = false
+    }
+
+    private fun startLatencyTracker() {
+        latencyTracker.onActionCancel(ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME)
+        val tag = if (statusBarStateController.isDozing) TRACE_TAG_AOD else TRACE_TAG_KEYGUARD
+        latencyTracker.onActionStart(ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME, tag)
+    }
+}
+
+private enum class FpsUnlockStage {
+    WAIT_FOR_AUTHENTICATION,
+    UI_READY,
+    HAL_AUTHENTICATION,
+    EXIT_KEYGUARD,
+    UNLOCKED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 10e7227..8f61dbf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -91,7 +91,8 @@
     @Main private val handler: Handler,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
     @Application private val scope: CoroutineScope,
-    dumpManager: DumpManager
+    dumpManager: DumpManager,
+    fpsUnlockTracker: FpsUnlockTracker
 ) : Dumpable {
     private val requests: HashSet<SideFpsUiRequestSource> = HashSet()
 
@@ -167,6 +168,7 @@
             }
 
     init {
+        fpsUnlockTracker.startTracking()
         fingerprintManager?.setSidefpsController(
             object : ISidefpsController.Stub() {
                 override fun show(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 3944ac2..4175937 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -24,6 +24,7 @@
 import static android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
 import static android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR;
 
+import static com.android.internal.util.LatencyTracker.ACTION_UDFPS_ILLUMINATE;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION;
 
@@ -167,6 +168,7 @@
     @NonNull private final InputManager mInputManager;
     @NonNull private final UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     @NonNull private final SelectedUserInteractor mSelectedUserInteractor;
+    @NonNull private final FpsUnlockTracker mFpsUnlockTracker;
     private final boolean mIgnoreRefreshRate;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
@@ -646,7 +648,8 @@
             @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
             @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate,
             @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider,
-            @NonNull SelectedUserInteractor selectedUserInteractor) {
+            @NonNull SelectedUserInteractor selectedUserInteractor,
+            @NonNull FpsUnlockTracker fpsUnlockTracker) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -690,6 +693,8 @@
         mInputManager = inputManager;
         mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate;
         mSelectedUserInteractor = selectedUserInteractor;
+        mFpsUnlockTracker = fpsUnlockTracker;
+        mFpsUnlockTracker.startTracking();
 
         mTouchProcessor = singlePointerTouchProcessor;
         mSessionTracker = sessionTracker;
@@ -974,7 +979,10 @@
             return;
         }
         if (isOptical()) {
-            mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+            mLatencyTracker.onActionStart(ACTION_UDFPS_ILLUMINATE);
+        }
+        if (getBiometricSessionType() == SESSION_KEYGUARD) {
+            mFpsUnlockTracker.onUiReadyStage();
         }
         // Refresh screen timeout and boost process priority if possible.
         mPowerManager.userActivity(mSystemClock.uptimeMillis(),
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index b8e2de4..c3421de 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.dagger
 
+import com.android.systemui.communal.data.db.CommunalDatabaseModule
 import com.android.systemui.communal.data.repository.CommunalRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
@@ -27,6 +28,7 @@
             CommunalRepositoryModule::class,
             CommunalTutorialRepositoryModule::class,
             CommunalWidgetRepositoryModule::class,
+            CommunalDatabaseModule::class,
         ]
 )
 class CommunalModule
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
new file mode 100644
index 0000000..595d320
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.data.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 1)
+abstract class CommunalDatabase : RoomDatabase() {
+    abstract fun communalWidgetDao(): CommunalWidgetDao
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt
new file mode 100644
index 0000000..e766290
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.data.db
+
+import android.content.Context
+import androidx.room.Room
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface CommunalDatabaseModule {
+    companion object {
+        @SysUISingleton
+        @Provides
+        fun provideCommunalDatabase(
+            @Application context: Context,
+            defaultWidgetPopulation: DefaultWidgetPopulation,
+        ): CommunalDatabase {
+            return Room.databaseBuilder(
+                    context,
+                    CommunalDatabase::class.java,
+                    context.resources.getString(R.string.config_communalDatabase)
+                )
+                .fallbackToDestructiveMigration()
+                .addCallback(defaultWidgetPopulation)
+                .build()
+        }
+
+        @SysUISingleton
+        @Provides
+        fun provideCommunalWidgetDao(database: CommunalDatabase): CommunalWidgetDao =
+            database.communalWidgetDao()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
new file mode 100644
index 0000000..0d5336a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.data.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "communal_widget_table")
+data class CommunalWidgetItem(
+    @PrimaryKey(autoGenerate = true) val uid: Long,
+    /** Id of an app widget */
+    @ColumnInfo(name = "widget_id") val widgetId: Int,
+    /** Component name of the app widget provider */
+    @ColumnInfo(name = "component_name") val componentName: String,
+    /** Reference the id of an item persisted in the glanceable hub */
+    @ColumnInfo(name = "item_id") val itemId: Long,
+)
+
+@Entity(tableName = "communal_item_rank_table")
+data class CommunalItemRank(
+    /** Unique id of an item persisted in the glanceable hub */
+    @PrimaryKey(autoGenerate = true) val uid: Long,
+    /** Order in which the item will be displayed */
+    @ColumnInfo(name = "rank", defaultValue = "0") val rank: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
new file mode 100644
index 0000000..e50850d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.data.db
+
+import android.content.ComponentName
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Query
+import androidx.room.RoomDatabase
+import androidx.room.Transaction
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule.Companion.DEFAULT_WIDGETS
+import com.android.systemui.communal.shared.CommunalWidgetHost
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Provider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Callback that will be invoked when the Room database is created. Then the database will be
+ * populated with pre-configured default widgets to be rendered in the glanceable hub.
+ */
+class DefaultWidgetPopulation
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val communalWidgetHost: CommunalWidgetHost,
+    private val communalWidgetDaoProvider: Provider<CommunalWidgetDao>,
+    @Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>,
+    @CommunalLog logBuffer: LogBuffer,
+) : RoomDatabase.Callback() {
+    companion object {
+        private const val TAG = "DefaultWidgetPopulation"
+    }
+    private val logger = Logger(logBuffer, TAG)
+
+    override fun onCreate(db: SupportSQLiteDatabase) {
+        super.onCreate(db)
+        applicationScope.launch {
+            addDefaultWidgets()
+            logger.i("Default widgets were populated in the database.")
+        }
+    }
+
+    // Read default widgets from config.xml and populate the database.
+    private suspend fun addDefaultWidgets() =
+        withContext(bgDispatcher) {
+            defaultWidgets.forEachIndexed { index, name ->
+                val provider = ComponentName.unflattenFromString(name)
+                provider?.let {
+                    val id = communalWidgetHost.allocateIdAndBindWidget(provider)
+                    id?.let {
+                        communalWidgetDaoProvider
+                            .get()
+                            .addWidget(
+                                widgetId = id,
+                                provider = provider,
+                                priority = defaultWidgets.size - index
+                            )
+                    }
+                }
+            }
+        }
+}
+
+@Dao
+interface CommunalWidgetDao {
+    @Query(
+        "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
+            "ON communal_item_rank_table.uid = communal_widget_table.item_id"
+    )
+    fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>
+
+    @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id")
+    fun getWidgetByIdNow(id: Int): CommunalWidgetItem
+
+    @Delete fun deleteWidgets(vararg widgets: CommunalWidgetItem)
+
+    @Query("DELETE FROM communal_item_rank_table WHERE uid = :itemId")
+    fun deleteItemRankById(itemId: Long)
+
+    @Query(
+        "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " +
+            "VALUES(:widgetId, :componentName, :itemId)"
+    )
+    fun insertWidget(widgetId: Int, componentName: String, itemId: Long): Long
+
+    @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
+    fun insertItemRank(rank: Int): Long
+
+    @Transaction
+    fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long {
+        return insertWidget(
+            widgetId = widgetId,
+            componentName = provider.flattenToString(),
+            insertItemRank(priority),
+        )
+    }
+
+    @Transaction
+    fun deleteWidgetById(widgetId: Int) {
+        val widget = getWidgetByIdNow(widgetId)
+        deleteItemRankById(widget.itemId)
+        deleteWidgets(widget)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt
deleted file mode 100644
index 1a214ba..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.data.model
-
-import com.android.systemui.communal.shared.model.CommunalContentSize
-
-/** Metadata for the default widgets */
-data class CommunalWidgetMetadata(
-    /* Widget provider component name */
-    val componentName: String,
-
-    /* Defines the order in which the widget will be rendered in the grid. */
-    val priority: Int,
-
-    /* Supported sizes */
-    val sizes: List<CommunalContentSize>
-)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
index 5c4ee35..b40570b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.communal.data.repository
 
 import com.android.systemui.Flags.communalHub
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
index 9d95b9e..1de3459 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.communal.data.repository
 
 import dagger.Binds
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 77025dc..6b27ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -28,47 +28,62 @@
 import android.os.UserManager
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.communal.data.model.CommunalWidgetMetadata
+import com.android.systemui.communal.data.db.CommunalItemRank
+import com.android.systemui.communal.data.db.CommunalWidgetDao
+import com.android.systemui.communal.data.db.CommunalWidgetItem
+import com.android.systemui.communal.shared.CommunalWidgetHost
 import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
-import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 
 /** Encapsulates the state of widgets for communal mode. */
 interface CommunalWidgetRepository {
     /** A flow of provider info for the stopwatch widget, or null if widget is unavailable. */
     val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?>
 
-    /** Widgets that are allowed to render in the glanceable hub */
-    val communalWidgetAllowlist: List<CommunalWidgetMetadata>
-
-    /** A flow of information about all the communal widgets to show. */
+    /** A flow of information about active communal widgets stored in database. */
     val communalWidgets: Flow<List<CommunalWidgetContentModel>>
+
+    /** Add a widget at the specified position in the app widget service and the database. */
+    fun addWidget(provider: ComponentName, priority: Int) {}
+
+    /** Delete a widget by id from app widget service and the database. */
+    fun deleteWidget(widgetId: Int) {}
 }
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class CommunalWidgetRepositoryImpl
 @Inject
 constructor(
-    @Application private val applicationContext: Context,
     private val appWidgetManager: AppWidgetManager,
     private val appWidgetHost: AppWidgetHost,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
     communalRepository: CommunalRepository,
+    private val communalWidgetHost: CommunalWidgetHost,
+    private val communalWidgetDao: CommunalWidgetDao,
     private val packageManager: PackageManager,
     private val userManager: UserManager,
     private val userTracker: UserTracker,
@@ -79,18 +94,12 @@
         const val TAG = "CommunalWidgetRepository"
         const val WIDGET_LABEL = "Stopwatch"
     }
-    override val communalWidgetAllowlist: List<CommunalWidgetMetadata>
 
     private val logger = Logger(logBuffer, TAG)
 
     // Whether the [AppWidgetHost] is listening for updates.
     private var isHostListening = false
 
-    init {
-        communalWidgetAllowlist =
-            if (communalRepository.isCommunalEnabled) getWidgetAllowlist() else emptyList()
-    }
-
     // Widgets that should be rendered in communal mode.
     private val widgets: HashMap<Int, CommunalAppWidgetInfo> = hashMapOf()
 
@@ -136,7 +145,6 @@
                 true
             } else {
                 stopListening()
-                clearWidgets()
                 false
             }
         }
@@ -157,57 +165,50 @@
                 return@map null
             }
 
-            return@map addWidget(providerInfo)
+            return@map addStopWatchWidget(providerInfo)
         }
 
     override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
-        isHostActive.map { isHostActive ->
+        isHostActive.flatMapLatest { isHostActive ->
             if (!isHostActive) {
-                return@map emptyList()
+                return@flatMapLatest flowOf(emptyList())
             }
-
-            // The allowlist should be fetched from the local database with all the metadata tied to
-            // a widget, including an appWidgetId if it has been bound. Before the database is set
-            // up, we are going to use the app widget host as the source of truth for bound widgets,
-            // and rebind each time on boot.
-
-            // Remove all previously bound widgets.
-            appWidgetHost.appWidgetIds.forEach { appWidgetHost.deleteAppWidgetId(it) }
-
-            val inventory = mutableListOf<CommunalWidgetContentModel>()
-
-            // Bind all widgets from the allowlist.
-            communalWidgetAllowlist.forEach {
-                val id = appWidgetHost.allocateAppWidgetId()
-                appWidgetManager.bindAppWidgetId(
-                    id,
-                    ComponentName.unflattenFromString(it.componentName),
-                )
-
-                inventory.add(
-                    CommunalWidgetContentModel(
-                        appWidgetId = id,
-                        providerInfo = appWidgetManager.getAppWidgetInfo(id),
-                        priority = it.priority,
-                    )
-                )
-            }
-
-            return@map inventory.toList()
+            communalWidgetDao.getWidgets().map { it.map(::mapToContentModel) }
         }
 
-    private fun getWidgetAllowlist(): List<CommunalWidgetMetadata> {
-        val componentNames =
-            applicationContext.resources.getStringArray(R.array.config_communalWidgetAllowlist)
-        return componentNames.mapIndexed { index, name ->
-            CommunalWidgetMetadata(
-                componentName = name,
-                priority = componentNames.size - index,
-                sizes = listOf(CommunalContentSize.HALF),
-            )
+    override fun addWidget(provider: ComponentName, priority: Int) {
+        applicationScope.launch(bgDispatcher) {
+            val id = communalWidgetHost.allocateIdAndBindWidget(provider)
+            id?.let {
+                communalWidgetDao.addWidget(
+                    widgetId = it,
+                    provider = provider,
+                    priority = priority,
+                )
+            }
+            logger.i("Added widget ${provider.flattenToString()} at position $priority.")
         }
     }
 
+    override fun deleteWidget(widgetId: Int) {
+        applicationScope.launch(bgDispatcher) {
+            communalWidgetDao.deleteWidgetById(widgetId)
+            appWidgetHost.deleteAppWidgetId(widgetId)
+            logger.i("Deleted widget with id $widgetId.")
+        }
+    }
+
+    private fun mapToContentModel(
+        entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
+    ): CommunalWidgetContentModel {
+        val (_, widgetId) = entry.value
+        return CommunalWidgetContentModel(
+            appWidgetId = widgetId,
+            providerInfo = appWidgetManager.getAppWidgetInfo(widgetId),
+            priority = entry.key.rank,
+        )
+    }
+
     private fun startListening() {
         if (isHostListening) {
             return
@@ -226,7 +227,8 @@
         isHostListening = false
     }
 
-    private fun addWidget(providerInfo: AppWidgetProviderInfo): CommunalAppWidgetInfo {
+    // TODO(b/306471933): remove this prototype that shows a stopwatch in the communal blueprint
+    private fun addStopWatchWidget(providerInfo: AppWidgetProviderInfo): CommunalAppWidgetInfo {
         val existing = widgets.values.firstOrNull { it.providerInfo == providerInfo }
         if (existing != null) {
             return existing
@@ -241,9 +243,4 @@
         widgets[appWidgetId] = widget
         return widget
     }
-
-    private fun clearWidgets() {
-        widgets.keys.forEach { appWidgetId -> appWidgetHost.deleteAppWidgetId(appWidgetId) }
-        widgets.clear()
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
index 3d1185b..5793f10 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
@@ -20,16 +20,24 @@
 import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
 import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.communal.shared.CommunalWidgetHost
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import javax.inject.Named
 
 @Module
 interface CommunalWidgetRepositoryModule {
     companion object {
         private const val APP_WIDGET_HOST_ID = 116
+        const val DEFAULT_WIDGETS = "default_widgets"
 
         @SysUISingleton
         @Provides
@@ -42,6 +50,22 @@
         fun provideAppWidgetHost(@Application context: Context): AppWidgetHost {
             return AppWidgetHost(context, APP_WIDGET_HOST_ID)
         }
+
+        @SysUISingleton
+        @Provides
+        fun provideCommunalWidgetHost(
+            appWidgetManager: AppWidgetManager,
+            appWidgetHost: AppWidgetHost,
+            @CommunalLog logBuffer: LogBuffer,
+        ): CommunalWidgetHost {
+            return CommunalWidgetHost(appWidgetManager, appWidgetHost, logBuffer)
+        }
+
+        @Provides
+        @Named(DEFAULT_WIDGETS)
+        fun provideDefaultWidgets(@Main resources: Resources): Array<String> {
+            return resources.getStringArray(R.array.config_communalWidgetAllowlist)
+        }
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index ccccbb6..2c683ee 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.domain.interactor
 
+import android.content.ComponentName
 import com.android.systemui.communal.data.repository.CommunalRepository
 import com.android.systemui.communal.data.repository.CommunalWidgetRepository
 import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
@@ -33,7 +34,7 @@
 @Inject
 constructor(
     private val communalRepository: CommunalRepository,
-    widgetRepository: CommunalWidgetRepository,
+    private val widgetRepository: CommunalWidgetRepository,
 ) {
 
     /** Whether communal features are enabled. */
@@ -68,4 +69,11 @@
     fun onSceneChanged(newScene: CommunalSceneKey) {
         communalRepository.setDesiredScene(newScene)
     }
+
+    /** Add a widget at the specified position. */
+    fun addWidget(componentName: ComponentName, priority: Int) =
+        widgetRepository.addWidget(componentName, priority)
+
+    /** Delete a widget by id. */
+    fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
new file mode 100644
index 0000000..086d729
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.shared
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import javax.inject.Inject
+
+/**
+ * Widget host that interacts with AppWidget service and host to manage and provide info for widgets
+ * shown in the glanceable hub.
+ */
+class CommunalWidgetHost
+@Inject
+constructor(
+    private val appWidgetManager: AppWidgetManager,
+    private val appWidgetHost: AppWidgetHost,
+    @CommunalLog logBuffer: LogBuffer,
+) {
+    companion object {
+        private const val TAG = "CommunalWidgetHost"
+    }
+    private val logger = Logger(logBuffer, TAG)
+
+    /**
+     * Allocate an app widget id and binds the widget.
+     *
+     * @return widgetId if binding is successful; otherwise return null
+     */
+    fun allocateIdAndBindWidget(provider: ComponentName): Int? {
+        val id = appWidgetHost.allocateAppWidgetId()
+        if (bindWidget(id, provider)) {
+            logger.d("Successfully bound the widget $provider")
+            return id
+        }
+        appWidgetHost.deleteAppWidgetId(id)
+        logger.d("Failed to bind the widget $provider")
+        return null
+    }
+
+    private fun bindWidget(widgetId: Int, provider: ComponentName): Boolean =
+        appWidgetManager.bindAppWidgetIdIfAllowed(widgetId, provider)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index de9b563..197dece 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.communal.ui.viewmodel
 
 import android.appwidget.AppWidgetHost
+import android.content.ComponentName
 import android.content.Context
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
@@ -61,4 +62,23 @@
     fun onSceneChanged(scene: CommunalSceneKey) {
         communalInteractor.onSceneChanged(scene)
     }
+
+    /** Delete a widget by id. */
+    fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
+
+    /** Open the widget picker */
+    fun onOpenWidgetPicker() {
+        // STOPSHIP(b/306500486): refactor this when integrating with the widget picker.
+        // Eventually clicking on this button will bring up the widget picker and inside
+        // the widget picker, addWidget will be called to add the user selected widget.
+        // For now, a stopwatch widget will be added to the end of the grid.
+        communalInteractor.addWidget(
+            componentName =
+                ComponentName(
+                    "com.google.android.deskclock",
+                    "com.android.alarmclock.StopwatchAppWidgetProvider"
+                ),
+            priority = 0
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
index 87b0f01..d500d1c2 100644
--- a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
@@ -20,7 +20,6 @@
 import android.view.View
 import android.widget.TextView
 import androidx.core.view.updatePadding
-import com.android.systemui.biometrics.Utils
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIBottomSheetDialog
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -36,6 +35,7 @@
     context: Context,
     private val onStartMirroringClickListener: View.OnClickListener,
     private val onCancelMirroring: View.OnClickListener,
+    private val navbarBottomInsetsProvider: () -> Int,
     configurationController: ConfigurationController? = null,
     theme: Int = R.style.Theme_SystemUI_Dialog,
 ) : SystemUIBottomSheetDialog(context, configurationController, theme) {
@@ -67,12 +67,12 @@
     private fun setupInsets() {
         // This avoids overlap between dialog content and navigation bars.
         requireViewById<View>(R.id.cd_bottom_sheet).apply {
-            val navbarInsets = Utils.getNavbarInsets(context)
+            val navbarInsets = navbarBottomInsetsProvider()
             val defaultDialogBottomInset =
                 context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding)
             // we only care about the bottom inset as in all other configuration where navigations
             // are in other display sides there is no overlap with the dialog.
-            updatePadding(bottom = max(navbarInsets.bottom, defaultDialogBottomInset))
+            updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset))
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
index 91f535d..19b4d22 100644
--- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
@@ -17,6 +17,7 @@
 
 import android.app.Dialog
 import android.content.Context
+import com.android.systemui.biometrics.Utils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -74,7 +75,8 @@
                         scope.launch(bgDispatcher) { pendingDisplay.ignore() }
                         hideDialog()
                     },
-                    configurationController
+                    navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom },
+                    configurationController,
                 )
                 .apply { show() }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e995c97..dd971b9 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -68,9 +68,6 @@
             "notification_drag_to_contents"
         )
 
-    // TODO(b/254512538): Tracking Bug
-    val INSTANT_VOICE_REPLY = unreleasedFlag("instant_voice_reply")
-
     /**
      * This flag controls whether we register a listener for StatsD notification memory reports.
      * For statsd to actually call the listener however, a server-side toggle needs to be
@@ -247,15 +244,6 @@
 
     /** Keyguard Migration */
 
-    /**
-     * Migrate the bottom area to the new keyguard root view. Because there is no such thing as a
-     * "bottom area" after this, this also breaks it up into many smaller, modular pieces.
-     */
-    // TODO(b/290652751): Tracking bug.
-    @JvmField
-    val MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA =
-        unreleasedFlag("migrate_split_keyguard_bottom_area")
-
     // TODO(b/297037052): Tracking bug.
     @JvmField
     val REMOVE_NPVC_BOTTOM_AREA_USAGE = unreleasedFlag("remove_npvc_bottom_area_usage")
@@ -267,10 +255,6 @@
     // TODO(b/287268101): Tracking bug.
     @JvmField val TRANSIT_CLOCK = releasedFlag("lockscreen_custom_transit_clock")
 
-    /** Migrate the lock icon view to the new keyguard root view. */
-    // TODO(b/286552209): Tracking bug.
-    @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag("migrate_lock_icon")
-
     // TODO(b/288276738): Tracking bug.
     @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag("widget_on_keyguard")
 
@@ -394,7 +378,7 @@
     @JvmField val SIGNAL_CALLBACK_DEPRECATION = releasedFlag("signal_callback_deprecation")
 
     // TODO(b/301610137): Tracking bug
-    @JvmField val NEW_NETWORK_SLICE_UI = unreleasedFlag("new_network_slice_ui", teamfood = true)
+    @JvmField val NEW_NETWORK_SLICE_UI = releasedFlag("new_network_slice_ui")
 
     // TODO(b/308138154): Tracking bug
     val FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS =
@@ -497,12 +481,6 @@
             namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER,
         )
 
-    // TODO(b/254512674): Tracking Bug
-    @Keep
-    @JvmField
-    val HIDE_NAVBAR_WINDOW =
-        sysPropBooleanFlag("persist.wm.debug.hide_navbar_window", default = false)
-
     @Keep
     @JvmField
     val WM_CAPTION_ON_SHELL =
@@ -790,11 +768,7 @@
 
     /** Enable the share wifi button in Quick Settings internet dialog. */
     @JvmField
-    val SHARE_WIFI_QS_BUTTON = unreleasedFlag("share_wifi_qs_button", teamfood = true)
-
-    /** Enable haptic slider component in the brightness slider */
-    @JvmField
-    val HAPTIC_BRIGHTNESS_SLIDER = unreleasedFlag("haptic_brightness_slider", teamfood = true)
+    val SHARE_WIFI_QS_BUTTON = releasedFlag("share_wifi_qs_button")
 
     // TODO(b/287205379): Tracking bug
     @JvmField
@@ -802,7 +776,7 @@
 
     /** Enable showing a dialog when clicking on Quick Settings bluetooth tile. */
     @JvmField
-    val BLUETOOTH_QS_TILE_DIALOG = unreleasedFlag("bluetooth_qs_tile_dialog", teamfood = true)
+    val BLUETOOTH_QS_TILE_DIALOG = unreleasedFlag("bluetooth_qs_tile_dialog")
 
     // TODO(b/300995746): Tracking Bug
     /** A resource flag for whether the communal service is enabled. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index e16bb0b..1e9be09 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -30,6 +30,7 @@
 import android.view.ViewGroup.MarginLayoutParams
 import android.view.Window
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
@@ -78,23 +79,29 @@
     private lateinit var stepProperties: StepViewProperties
 
     @ColorInt
-    var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
-    @ColorInt
-    var emptyRectangleColor =
-        getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
-    @ColorInt
-    var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
-    @ColorInt
-    var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
-    @ColorInt
-    var defaultIconBackgroundColor =
+    private val filledRectangleColor =
         getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
     @ColorInt
-    var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    private val emptyRectangleColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
     @ColorInt
-    var dimmedIconBackgroundColor =
+    private val backgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
+    @ColorInt
+    private val defaultIconColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+    @ColorInt
+    private val defaultIconBackgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
+    @ColorInt
+    private val dimmedIconColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    @ColorInt
+    private val dimmedIconBackgroundColor =
         getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
 
+    private val levelContentDescription = context.getString(R.string.keyboard_backlight_value)
+
     init {
         currentLevel = initialCurrentLevel
         maxLevel = initialMaxLevel
@@ -103,6 +110,8 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         setUpWindowProperties(this)
         setWindowPosition()
+        // title is used for a11y announcement
+        window?.setTitle(context.getString(R.string.keyboard_backlight_dialog_title))
         updateResources()
         rootView = buildRootView()
         setContentView(rootView)
@@ -159,6 +168,12 @@
         currentLevel = current
         updateIconTile()
         updateStepColors()
+        updateAccessibilityInfo()
+    }
+
+    private fun updateAccessibilityInfo() {
+        rootView.contentDescription = String.format(levelContentDescription, currentLevel, maxLevel)
+        rootView.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
     }
 
     private fun updateIconTile() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 61c8e1bb..1037b0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -27,6 +27,7 @@
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardStatusViewComponent
 import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
@@ -113,7 +114,7 @@
     fun bindIndicationArea() {
         indicationAreaHandle?.dispose()
 
-        if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (!keyguardBottomAreaRefactor()) {
             keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let {
                 keyguardRootView.removeView(it)
             }
@@ -125,7 +126,6 @@
                 keyguardIndicationAreaViewModel,
                 keyguardRootViewModel,
                 indicationController,
-                featureFlags,
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/InWindowLauncherUnlockAnimationRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/InWindowLauncherUnlockAnimationRepository.kt
new file mode 100644
index 0000000..d23899b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/InWindowLauncherUnlockAnimationRepository.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
+import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
+import com.android.systemui.shared.system.smartspace.SmartspaceState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * State related to System UI's handling of the in-window Launcher unlock animations. This includes
+ * the staggered icon entry animation that plays during unlock, as well as the smartspace shared
+ * element animation, if supported.
+ *
+ * While the animations themselves occur fully in the Launcher window, System UI is responsible for
+ * preparing/starting the animations, as well as synchronizing the smartspace state so that the two
+ * smartspaces appear visually identical for the shared element animation.
+ */
+@SysUISingleton
+class InWindowLauncherUnlockAnimationRepository @Inject constructor() {
+
+    /**
+     * Whether we have called [ILauncherUnlockAnimationController.playUnlockAnimation] during this
+     * unlock sequence. This value is set back to false once
+     * [InWindowLauncherUnlockAnimationInteractor.shouldStartInWindowAnimation] reverts to false,
+     * which happens when we're no longer in transition to GONE or if the remote animation ends or
+     * is cancelled.
+     */
+    val startedUnlockAnimation = MutableStateFlow(false)
+
+    /**
+     * The unlock amount we've explicitly passed to
+     * [ILauncherUnlockAnimationController.setUnlockAmount]. This is used whenever System UI is
+     * directly controlling the amount of the unlock animation, such as during a manual swipe to
+     * unlock gesture.
+     *
+     * This value is *not* updated if we called
+     * [ILauncherUnlockAnimationController.playUnlockAnimation] to ask Launcher to animate all the
+     * way unlocked, since that animator is running in the Launcher window.
+     */
+    val manualUnlockAmount: MutableStateFlow<Float?> = MutableStateFlow(null)
+
+    /**
+     * The class name of the Launcher activity that provided us with a
+     * [ILauncherUnlockAnimationController], if applicable. We can use this to check if that
+     * launcher is underneath the lockscreen before playing in-window animations.
+     *
+     * If null, we have not been provided with a launcher unlock animation controller.
+     */
+    val launcherActivityClass: MutableStateFlow<String?> = MutableStateFlow(null)
+
+    /**
+     * Information about the Launcher's smartspace, which is passed to us via
+     * [ILauncherUnlockAnimationController].
+     */
+    val launcherSmartspaceState: MutableStateFlow<SmartspaceState?> = MutableStateFlow(null)
+
+    fun setStartedUnlockAnimation(started: Boolean) {
+        startedUnlockAnimation.value = started
+    }
+
+    fun setManualUnlockAmount(amount: Float?) {
+        manualUnlockAmount.value = amount
+    }
+
+    fun setLauncherActivityClass(className: String) {
+        launcherActivityClass.value = className
+    }
+
+    fun setLauncherSmartspaceState(state: SmartspaceState?) {
+        launcherSmartspaceState.value = state
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt
index 014b7fa..6121b633 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSurfaceBehindRepository.kt
@@ -31,8 +31,14 @@
     /** Whether we're running animations on the surface. */
     val isAnimatingSurface: Flow<Boolean>
 
+    /** Whether we have a RemoteAnimationTarget to run animations on the surface. */
+    val isSurfaceRemoteAnimationTargetAvailable: Flow<Boolean>
+
     /** Set whether we're running animations on the surface. */
     fun setAnimatingSurface(animating: Boolean)
+
+    /** Set whether we have a RemoteAnimationTarget with which to run animations on the surface. */
+    fun setSurfaceRemoteAnimationTargetAvailable(available: Boolean)
 }
 
 @SysUISingleton
@@ -40,7 +46,15 @@
     private val _isAnimatingSurface = MutableStateFlow(false)
     override val isAnimatingSurface = _isAnimatingSurface.asStateFlow()
 
+    private val _isSurfaceRemoteAnimationTargetAvailable = MutableStateFlow(false)
+    override val isSurfaceRemoteAnimationTargetAvailable =
+        _isSurfaceRemoteAnimationTargetAvailable.asStateFlow()
+
     override fun setAnimatingSurface(animating: Boolean) {
         _isAnimatingSurface.value = animating
     }
+
+    override fun setSurfaceRemoteAnimationTargetAvailable(available: Boolean) {
+        _isSurfaceRemoteAnimationTargetAvailable.value = available
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index c4962a1..ea40ba0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.util.kotlin.Utils.Companion.toQuad
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
+import dagger.Lazy
 import java.util.UUID
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -56,6 +57,7 @@
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
     private val powerInteractor: PowerInteractor,
+    inWindowLauncherUnlockAnimationInteractor: Lazy<InWindowLauncherUnlockAnimationInteractor>,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.LOCKSCREEN,
@@ -104,12 +106,21 @@
     val surfaceBehindModel: Flow<KeyguardSurfaceBehindModel?> =
         combine(
                 transitionInteractor.startedKeyguardTransitionStep,
-                transitionInteractor.transitionStepsFromState(KeyguardState.LOCKSCREEN)
-            ) { startedStep, fromLockscreenStep ->
+                transitionInteractor.transitionStepsFromState(KeyguardState.LOCKSCREEN),
+                inWindowLauncherUnlockAnimationInteractor
+                    .get()
+                    .transitioningToGoneWithInWindowAnimation,
+            ) { startedStep, fromLockscreenStep, transitioningToGoneWithInWindowAnimation ->
                 if (startedStep.to != KeyguardState.GONE) {
                     // Only LOCKSCREEN -> GONE has specific surface params (for the unlock
                     // animation).
                     return@combine null
+                } else if (transitioningToGoneWithInWindowAnimation) {
+                    // If we're prepared for the in-window unlock, we're going to play an animation
+                    // in the window. Make it fully visible.
+                    KeyguardSurfaceBehindModel(
+                        alpha = 1f,
+                    )
                 } else if (fromLockscreenStep.value > 0.5f) {
                     // Start the animation once we're 50% transitioned to GONE.
                     KeyguardSurfaceBehindModel(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt
new file mode 100644
index 0000000..e7d74a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractor.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
+import com.android.systemui.keyguard.data.repository.KeyguardSurfaceBehindRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.shared.system.smartspace.SmartspaceState
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class InWindowLauncherUnlockAnimationInteractor
+@Inject
+constructor(
+    private val repository: InWindowLauncherUnlockAnimationRepository,
+    @Application scope: CoroutineScope,
+    transitionInteractor: KeyguardTransitionInteractor,
+    surfaceBehindRepository: dagger.Lazy<KeyguardSurfaceBehindRepository>,
+    private val activityManager: ActivityManagerWrapper,
+) {
+    val startedUnlockAnimation = repository.startedUnlockAnimation.asStateFlow()
+
+    /**
+     * Whether we've STARTED but not FINISHED a transition to GONE, and the preconditions are met to
+     * play the in-window unlock animation.
+     */
+    val transitioningToGoneWithInWindowAnimation: StateFlow<Boolean> =
+        transitionInteractor
+            .isInTransitionToState(KeyguardState.GONE)
+            .sample(repository.launcherActivityClass, ::Pair)
+            .map { (isTransitioningToGone, launcherActivityClass) ->
+                isTransitioningToGone && isActivityClassUnderneath(launcherActivityClass)
+            }
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    /**
+     * Whether we should start the in-window unlock animation.
+     *
+     * This emits true once the Launcher surface becomes available while we're
+     * [transitioningToGoneWithInWindowAnimation].
+     */
+    val shouldStartInWindowAnimation: StateFlow<Boolean> =
+        combine(
+                transitioningToGoneWithInWindowAnimation,
+                surfaceBehindRepository.get().isSurfaceRemoteAnimationTargetAvailable,
+            ) { transitioningWithInWindowAnimation, isSurfaceAvailable ->
+                transitioningWithInWindowAnimation && isSurfaceAvailable
+            }
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    /** Sets whether we've started */
+    fun setStartedUnlockAnimation(started: Boolean) {
+        repository.setStartedUnlockAnimation(started)
+    }
+
+    fun setManualUnlockAmount(amount: Float) {
+        repository.setManualUnlockAmount(amount)
+    }
+
+    fun setLauncherActivityClass(className: String) {
+        repository.setLauncherActivityClass(className)
+    }
+
+    fun setLauncherSmartspaceState(state: SmartspaceState?) {
+        repository.setLauncherSmartspaceState(state)
+    }
+
+    /**
+     * Whether an activity with the given [activityClass] name is currently underneath the
+     * lockscreen (it's at the top of the activity task stack).
+     */
+    private fun isActivityClassUnderneath(activityClass: String?): Boolean {
+        return activityClass?.let {
+            activityManager.runningTask?.topActivity?.className?.equals(it)
+        }
+            ?: false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
index bf04f8f..efbe261 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -20,13 +20,13 @@
 import com.android.systemui.keyguard.data.repository.KeyguardSurfaceBehindRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
+import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import javax.inject.Inject
 
 @SysUISingleton
 class KeyguardSurfaceBehindInteractor
@@ -40,19 +40,18 @@
 
     @OptIn(ExperimentalCoroutinesApi::class)
     val viewParams: Flow<KeyguardSurfaceBehindModel> =
-        transitionInteractor.isInTransitionToAnyState
-            .flatMapLatest { isInTransition ->
-                if (!isInTransition) {
-                    defaultParams
-                } else {
-                    combine(
-                        transitionSpecificViewParams,
-                        defaultParams,
-                    ) { transitionParams, defaultParams ->
-                        transitionParams ?: defaultParams
-                    }
+        transitionInteractor.isInTransitionToAnyState.flatMapLatest { isInTransition ->
+            if (!isInTransition) {
+                defaultParams
+            } else {
+                combine(
+                    transitionSpecificViewParams,
+                    defaultParams,
+                ) { transitionParams, defaultParams ->
+                    transitionParams ?: defaultParams
                 }
             }
+        }
 
     val isAnimatingSurface = repository.isAnimatingSurface
 
@@ -86,4 +85,8 @@
     fun setAnimatingSurface(animating: Boolean) {
         repository.setAnimatingSurface(animating)
     }
+
+    fun setSurfaceRemoteAnimationTargetAvailable(available: Boolean) {
+        repository.setSurfaceRemoteAnimationTargetAvailable(available)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherAnimationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherAnimationViewBinder.kt
new file mode 100644
index 0000000..56a6e9b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherAnimationViewBinder.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
+import com.android.systemui.keyguard.ui.viewmodel.InWindowLauncherAnimationViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Binds the [InWindowLauncherUnlockAnimationManager] "view", which manages the lifecycle and state
+ * of the in-window Launcher animation.
+ */
+object InWindowLauncherAnimationViewBinder {
+
+    @JvmStatic
+    fun bind(
+        viewModel: InWindowLauncherAnimationViewModel,
+        inWindowLauncherUnlockAnimationManager: InWindowLauncherUnlockAnimationManager,
+        scope: CoroutineScope
+    ) {
+        scope.launch {
+            viewModel.shouldPrepareForInWindowAnimation.collect { shouldPrepare ->
+                if (shouldPrepare) {
+                    inWindowLauncherUnlockAnimationManager.prepareForUnlock()
+                } else {
+                    // If we no longer meet the conditions to prepare for unlock, we'll need to
+                    // manually set Launcher unlocked if we didn't start the unlock animation, or it
+                    // will remain "prepared" (blank) forever.
+                    inWindowLauncherUnlockAnimationManager.ensureUnlockedOrAnimatingUnlocked()
+                }
+            }
+        }
+
+        scope.launch {
+            viewModel.shouldStartInWindowAnimation.collect { shouldStart ->
+                if (shouldStart) {
+                    inWindowLauncherUnlockAnimationManager.playUnlockAnimation(unlocked = true)
+                } else {
+                    // Once the conditions to start the animation are no longer met, clear whether
+                    // we started the animation, since we'll need to start it again if the
+                    // conditions become true again.
+                    inWindowLauncherUnlockAnimationManager.clearStartedUnlockAnimation()
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index f20a666..1a8f625 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -22,9 +22,8 @@
 import android.widget.TextView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -54,7 +53,6 @@
         viewModel: KeyguardIndicationAreaViewModel,
         keyguardRootViewModel: KeyguardRootViewModel,
         indicationController: KeyguardIndicationController,
-        featureFlags: FeatureFlags,
     ): DisposableHandle {
         val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area)
         indicationController.setIndicationArea(indicationArea)
@@ -71,7 +69,7 @@
             view.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.STARTED) {
                     launch {
-                        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+                        if (keyguardBottomAreaRefactor()) {
                             keyguardRootViewModel.alpha.collect { alpha ->
                                 indicationArea.apply {
                                     this.importantForAccessibility =
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 378656c..1f74bb6 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
@@ -33,6 +33,7 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
 import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.TintedIcon
@@ -111,7 +112,7 @@
                         }
                     }
 
-                    if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+                    if (keyguardBottomAreaRefactor()) {
                         launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } }
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
index c8dab32..8587022 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
@@ -51,6 +51,11 @@
     private val interactor: KeyguardSurfaceBehindInteractor,
 ) {
     private var surfaceBehind: RemoteAnimationTarget? = null
+        set(value) {
+            field = value
+            interactor.setSurfaceRemoteAnimationTargetAvailable(value != null)
+        }
+
     private val surfaceTransactionApplier: SyncRtSurfaceTransactionApplier
         get() = SyncRtSurfaceTransactionApplier(keyguardViewController.viewRootImpl.view)
 
@@ -66,7 +71,7 @@
                     dampingRatio = 1f
                 }
             addUpdateListener { _, _, _ -> applyToSurfaceBehind() }
-            addEndListener { _, _, _, _ -> 
+            addEndListener { _, _, _, _ ->
                 try {
                     updateIsAnimatingSurface()
                 } catch (e: NullPointerException) {
@@ -112,6 +117,7 @@
     fun applyParamsToSurface(surface: RemoteAnimationTarget) {
         this.surfaceBehind = surface
         startOrUpdateAnimators()
+        applyToSurfaceBehind()
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index b797c4b..bdd9a6bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -41,6 +41,7 @@
 import androidx.core.view.isInvisible
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -156,7 +157,7 @@
     private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>()
 
     init {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             keyguardRootViewModel.enablePreviewMode()
             quickAffordancesCombinedViewModel.enablePreviewMode(
                 initiallySelectedSlotId =
@@ -199,7 +200,7 @@
 
             setupKeyguardRootView(previewContext, rootView)
 
-            if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+            if (!keyguardBottomAreaRefactor()) {
                 setUpBottomArea(rootView)
             }
 
@@ -243,7 +244,7 @@
     }
 
     fun onSlotSelected(slotId: String) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
         } else {
             bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
@@ -254,7 +255,7 @@
         isDestroyed = true
         lockscreenSmartspaceController.disconnect()
         disposables.forEach { it.dispose() }
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             shortcutsBindings.forEach { it.destroy() }
         }
     }
@@ -363,7 +364,7 @@
 
         disposables.add(
             PreviewKeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) {
-                if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+                if (keyguardBottomAreaRefactor()) {
                     setupShortcuts(keyguardRootView)
                 }
                 setUpUdfps(previewContext, rootView)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt
new file mode 100644
index 0000000..eb005f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt
@@ -0,0 +1,199 @@
+/*
+ *
+ *  * Copyright (C) 2023 The Android Open Source Project
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ *
+ */
+package com.android.systemui.keyguard.ui.view
+
+import android.graphics.Rect
+import android.util.Log
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
+import com.android.systemui.keyguard.ui.binder.InWindowLauncherAnimationViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.InWindowLauncherAnimationViewModel
+import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
+import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController
+import com.android.systemui.shared.system.smartspace.SmartspaceState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+private val TAG = InWindowLauncherUnlockAnimationManager::class.simpleName
+private const val UNLOCK_ANIMATION_DURATION = 633L
+private const val UNLOCK_START_DELAY = 100L
+
+/**
+ * Handles interactions between System UI and Launcher related to the in-window unlock animation.
+ *
+ * Launcher registers its unlock controller with us here, and we use that to prepare for and start
+ * the unlock animation.
+ */
+@SysUISingleton
+class InWindowLauncherUnlockAnimationManager
+@Inject
+constructor(
+    val interactor: InWindowLauncherUnlockAnimationInteractor,
+    val viewModel: InWindowLauncherAnimationViewModel,
+    @Application val scope: CoroutineScope,
+) : ISysuiUnlockAnimationController.Stub() {
+
+    /**
+     * The smartspace view on the lockscreen. This is used to perform the shared element animation
+     * between the lockscreen smartspace and the launcher one.
+     */
+    var lockscreenSmartspace: View? = null
+
+    private var launcherAnimationController: ILauncherUnlockAnimationController? = null
+
+    /**
+     * Whether we've called [ILauncherUnlockAnimationController.prepareForUnlock], and have *not*
+     * subsequently called [ILauncherUnlockAnimationController.playUnlockAnimation] or
+     * [ILauncherUnlockAnimationController.setUnlockAmount].
+     */
+    private var preparedForUnlock = false
+
+    /**
+     * Most recent value passed to [ILauncherUnlockAnimationController.setUnlockAmount] during this
+     * unlock.
+     *
+     * Null if we have not set a manual unlock amount, or once [ensureUnlockedOrAnimatingUnlocked]
+     * has been called.
+     */
+    private var manualUnlockAmount: Float? = null
+
+    /**
+     * Called from [OverviewProxyService] to provide us with the launcher unlock animation
+     * controller, which can be used to start and update the unlock animation in the launcher
+     * process.
+     */
+    override fun setLauncherUnlockController(
+        activityClass: String,
+        launcherController: ILauncherUnlockAnimationController,
+    ) {
+        interactor.setLauncherActivityClass(activityClass)
+        launcherAnimationController = launcherController
+
+        // Bind once we have a launcher controller.
+        InWindowLauncherAnimationViewBinder.bind(viewModel, this, scope)
+    }
+
+    /**
+     * Called from the launcher process when their smartspace state updates something we should know
+     * about.
+     */
+    override fun onLauncherSmartspaceStateUpdated(state: SmartspaceState?) {
+        interactor.setLauncherSmartspaceState(state)
+    }
+
+    /**
+     * Requests that the launcher prepare for unlock by becoming blank and optionally positioning
+     * its smartspace at the same position as the lockscreen smartspace.
+     *
+     * This state is dangerous - the launcher will remain blank until we ask it to animate unlocked,
+     * either via [playUnlockAnimation] or [setUnlockAmount]. If you don't want to get funny but bad
+     * bugs titled "tiny launcher" or "Expected: launcher icons; Actual: no icons ever", be very
+     * careful here.
+     */
+    fun prepareForUnlock() {
+        launcherAnimationController?.let { launcher ->
+            if (!preparedForUnlock) {
+                preparedForUnlock = true
+                manualUnlockAmount = null
+
+                launcher.prepareForUnlock(
+                    false,
+                    Rect(),
+                    0
+                ) // TODO(b/293894758): Add smartspace animation support.
+            }
+        }
+    }
+
+    /** Ensures that the launcher is either fully visible, or animating to be fully visible. */
+    fun ensureUnlockedOrAnimatingUnlocked() {
+        val preparedButDidNotStartAnimation =
+            preparedForUnlock && !interactor.startedUnlockAnimation.value
+        val manualUnlockSetButNotFullyVisible =
+            manualUnlockAmount != null && manualUnlockAmount != 1f
+
+        if (preparedButDidNotStartAnimation) {
+            Log.e(
+                TAG,
+                "Called prepareForUnlock(), but not playUnlockAnimation(). " +
+                    "Failing-safe by calling setUnlockAmount(1f)"
+            )
+            setUnlockAmount(1f, forceIfAnimating = true)
+        } else if (manualUnlockSetButNotFullyVisible) {
+            Log.e(
+                TAG,
+                "Unlock has ended, but manual unlock amount != 1f. " +
+                    "Failing-safe by calling setUnlockAmount(1f)"
+            )
+            setUnlockAmount(1f, forceIfAnimating = true)
+        }
+
+        manualUnlockAmount = null // Un-set the manual unlock amount as we're now visible.
+    }
+
+    /**
+     * Asks launcher to play the in-window unlock animation with the specified parameters.
+     *
+     * Once this is called, we're no longer [preparedForUnlock] as unlock is underway.
+     */
+    fun playUnlockAnimation(
+        unlocked: Boolean,
+        duration: Long = UNLOCK_ANIMATION_DURATION,
+        startDelay: Long = UNLOCK_START_DELAY,
+    ) {
+        if (preparedForUnlock) {
+            launcherAnimationController?.let { launcher ->
+                launcher.playUnlockAnimation(unlocked, duration, startDelay)
+                interactor.setStartedUnlockAnimation(true)
+            }
+        } else {
+            Log.e(TAG, "Attempted to call playUnlockAnimation() before prepareToUnlock().")
+        }
+
+        preparedForUnlock = false
+    }
+
+    /**
+     * Clears the played unlock animation flag. Since we don't have access to an onAnimationEnd
+     * event for the launcher animation (since it's in a different process), this is called whenever
+     * the transition to GONE ends or the surface becomes unavailable. In both cases, we'd need to
+     * play the animation next time we unlock.
+     */
+    fun clearStartedUnlockAnimation() {
+        interactor.setStartedUnlockAnimation(false)
+    }
+
+    /**
+     * Manually sets the unlock amount on launcher. This is used to explicitly set us to fully
+     * unlocked, or to manually control the animation (such as during a swipe to unlock).
+     *
+     * Once this is called, we're no longer [preparedForUnlock] since the Launcher icons are not
+     * configured to be invisible for the start of the unlock animation.
+     */
+    fun setUnlockAmount(amount: Float, forceIfAnimating: Boolean) {
+        preparedForUnlock = false
+
+        launcherAnimationController?.let {
+            manualUnlockAmount = amount
+            it.setUnlockAmount(amount, forceIfAnimating)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
index 28e6a95..eb01d4f6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
@@ -25,10 +25,9 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -41,7 +40,6 @@
 @Inject
 constructor(
     @Main private val resources: Resources,
-    private val featureFlags: FeatureFlags,
     private val keyguardQuickAffordancesCombinedViewModel:
         KeyguardQuickAffordancesCombinedViewModel,
     private val keyguardRootViewModel: KeyguardRootViewModel,
@@ -50,14 +48,14 @@
     private val vibratorHelper: VibratorHelper,
 ) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             addLeftShortcut(constraintLayout)
             addRightShortcut(constraintLayout)
         }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             leftShortcutHandle =
                 KeyguardQuickAffordanceViewBinder.bind(
                     constraintLayout.requireViewById(R.id.start_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt
index 9371d4e..20cb9b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt
@@ -29,9 +29,8 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardAmbientIndicationAreaViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel
@@ -42,14 +41,13 @@
 @Inject
 constructor(
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val featureFlags: FeatureFlags,
     private val keyguardAmbientIndicationViewModel: KeyguardAmbientIndicationViewModel,
     private val keyguardRootViewModel: KeyguardRootViewModel,
 ) : KeyguardSection() {
     private var ambientIndicationAreaHandle: KeyguardAmbientIndicationAreaViewBinder.Binding? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             val view =
                 LayoutInflater.from(constraintLayout.context)
                     .inflate(R.layout.ambient_indication, constraintLayout, false)
@@ -59,7 +57,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             ambientIndicationAreaHandle =
                 KeyguardAmbientIndicationAreaViewBinder.bind(
                     constraintLayout,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
index 755549b..ace970a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
@@ -29,6 +29,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconView
 import com.android.keyguard.LockIconViewController
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -60,7 +61,7 @@
     private val deviceEntryIconViewId = R.id.device_entry_icon_view
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON) &&
+        if (!keyguardBottomAreaRefactor() &&
                 !featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)
         ) {
             return
@@ -74,7 +75,7 @@
             if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
                 DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
             } else {
-                // Flags.MIGRATE_LOCK_ICON
+                // keyguardBottomAreaRefactor()
                 LockIconView(context, null).apply { id = R.id.lock_icon_view }
             }
         constraintLayout.addView(view)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 623eac0..8aef7c2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -21,9 +21,8 @@
 import android.view.ViewGroup
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
 import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
@@ -40,27 +39,25 @@
     private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
     private val keyguardRootViewModel: KeyguardRootViewModel,
     private val indicationController: KeyguardIndicationController,
-    private val featureFlags: FeatureFlags,
 ) : KeyguardSection() {
     private val indicationAreaViewId = R.id.keyguard_indication_area
     private var indicationAreaHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             val view = KeyguardIndicationArea(context, null)
             constraintLayout.addView(view)
         }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             indicationAreaHandle =
                 KeyguardIndicationAreaBinder.bind(
                     constraintLayout,
                     keyguardIndicationAreaViewModel,
                     keyguardRootViewModel,
                     indicationController,
-                    featureFlags,
                 )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
index 6fd13e0..9a33f08 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
@@ -28,6 +28,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import androidx.core.view.isVisible
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
 import com.android.systemui.animation.view.LaunchableLinearLayout
 import com.android.systemui.dagger.qualifiers.Main
@@ -45,7 +46,6 @@
 @Inject
 constructor(
     @Main private val resources: Resources,
-    private val featureFlags: FeatureFlags,
     private val keyguardSettingsMenuViewModel: KeyguardSettingsMenuViewModel,
     private val vibratorHelper: VibratorHelper,
     private val activityStarter: ActivityStarter,
@@ -53,7 +53,7 @@
     private var settingsPopupMenuHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (!keyguardBottomAreaRefactor()) {
             return
         }
         val view =
@@ -68,7 +68,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             settingsPopupMenuHandle =
                 KeyguardSettingsViewBinder.bind(
                     constraintLayout.requireViewById<View>(R.id.keyguard_settings_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index a679120..0f6a966 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -24,6 +24,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.LEFT
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
@@ -40,7 +41,6 @@
 @Inject
 constructor(
     @Main private val resources: Resources,
-    private val featureFlags: FeatureFlags,
     private val keyguardQuickAffordancesCombinedViewModel:
         KeyguardQuickAffordancesCombinedViewModel,
     private val keyguardRootViewModel: KeyguardRootViewModel,
@@ -49,14 +49,14 @@
     private val vibratorHelper: VibratorHelper,
 ) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             addLeftShortcut(constraintLayout)
             addRightShortcut(constraintLayout)
         }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             leftShortcutHandle =
                 KeyguardQuickAffordanceViewBinder.bind(
                     constraintLayout.requireViewById(R.id.start_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/InWindowLauncherAnimationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/InWindowLauncherAnimationViewModel.kt
new file mode 100644
index 0000000..2807558
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/InWindowLauncherAnimationViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
+import javax.inject.Inject
+
+@SysUISingleton
+class InWindowLauncherAnimationViewModel
+@Inject
+constructor(interactor: InWindowLauncherUnlockAnimationInteractor) {
+
+    /**
+     * Whether we should call [ILauncherUnlockAnimationController.prepareForUnlock] to set up the
+     * Launcher icons for the in-window unlock.
+     *
+     * We'll do this as soon as we're transitioning to GONE when the necessary preconditions are
+     * met.
+     */
+    val shouldPrepareForInWindowAnimation = interactor.transitioningToGoneWithInWindowAnimation
+
+    /**
+     * Whether we should call [ILauncherUnlockAnimationController.playUnlockAnimation] to start the
+     * in-window unlock animation.
+     */
+    val shouldStartInWindowAnimation = interactor.shouldStartInWindowAnimation
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index 980cc1b..2327c02 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -16,9 +16,8 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import javax.inject.Inject
@@ -36,7 +35,6 @@
     keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
     private val burnInHelperWrapper: BurnInHelperWrapper,
     private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
-    private val featureFlags: FeatureFlags,
 ) {
 
     /** Notifies when a new configuration is set */
@@ -47,7 +45,7 @@
 
     /** An observable for whether the indication area should be padded. */
     val isIndicationAreaPadded: Flow<Boolean> =
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) {
                 startButtonModel,
                 endButtonModel ->
@@ -64,7 +62,7 @@
         }
     /** An observable for the x-offset by which the indication area should be translated. */
     val indicationAreaTranslationX: Flow<Float> =
-        if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
         } else {
             bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index a985236..5e3a166 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -1383,7 +1383,17 @@
         args.putInt(
                 AssistManager.INVOCATION_TYPE_KEY,
                 AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
-        mAssistManagerLazy.get().startAssist(args);
+        // If Launcher has requested to override long press home, add a delay for the ripple.
+        // TODO(b/304146255): Remove this delay once we can exclude 3-button nav from screenshot.
+        boolean delayAssistInvocation = mAssistManagerLazy.get().shouldOverrideAssist(
+                AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
+        // In practice, I think v should always be a KeyButtonView, but just being safe.
+        if (delayAssistInvocation && v instanceof KeyButtonView) {
+            ((KeyButtonView) v).setOnRippleInvisibleRunnable(
+                    () -> mAssistManagerLazy.get().startAssist(args));
+        } else {
+            mAssistManagerLazy.get().startAssist(args);
+        }
         mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::awakenDreams);
         mView.abortCurrentGesture();
         return true;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index 2928cce..79aedff 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -21,6 +21,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
 import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
+import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
 
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -49,8 +50,6 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.settings.DisplayTracker;
@@ -83,7 +82,6 @@
     private final Context mContext;
     private final Handler mHandler;
     private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
-    private FeatureFlags mFeatureFlags;
     private final SecureSettings mSecureSettings;
     private final DisplayTracker mDisplayTracker;
     private final DisplayManager mDisplayManager;
@@ -118,13 +116,11 @@
             TaskStackChangeListeners taskStackChangeListeners,
             Optional<Pip> pipOptional,
             Optional<BackAnimation> backAnimation,
-            FeatureFlags featureFlags,
             SecureSettings secureSettings,
             DisplayTracker displayTracker) {
         mContext = context;
         mHandler = mainHandler;
         mNavigationBarComponentFactory = navigationBarComponentFactory;
-        mFeatureFlags = featureFlags;
         mSecureSettings = secureSettings;
         mDisplayTracker = displayTracker;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
@@ -248,8 +244,8 @@
     /** @return {@code true} if taskbar is enabled, false otherwise */
     private boolean initializeTaskbarIfNecessary() {
         // Enable for large screens or (phone AND flag is set); assuming phone = !mIsLargeScreen
-        boolean taskbarEnabled = (mIsLargeScreen || mFeatureFlags.isEnabled(
-                Flags.HIDE_NAVBAR_WINDOW)) && shouldCreateNavBarAndTaskBar(mContext.getDisplayId());
+        boolean taskbarEnabled = (mIsLargeScreen || enableTaskbarNavbarUnification())
+                && shouldCreateNavBarAndTaskBar(mContext.getDisplayId());
 
         if (taskbarEnabled) {
             Trace.beginSection("NavigationBarController#initializeTaskbarIfNecessary");
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
index bc4f7f25..258208d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
@@ -62,7 +62,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.settingslib.Utils;
 import com.android.systemui.Gefingerpoken;
-import com.android.systemui.res.R;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
 import com.android.systemui.navigationbar.buttons.ContextualButton;
@@ -73,6 +72,7 @@
 import com.android.systemui.navigationbar.buttons.RotationContextButton;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index dcf1a8e..6ec46f6 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -58,8 +58,9 @@
 import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.Dependency;
-import com.android.systemui.res.R;
+import com.android.systemui.assist.AssistManager;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.res.R;
 import com.android.systemui.shared.system.QuickStepContract;
 
 public class KeyButtonView extends ImageView implements ButtonInterface {
@@ -439,11 +440,22 @@
         if (mCode != KeyEvent.KEYCODE_UNKNOWN) {
             sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
         }
+        // When aborting long-press home and Launcher has requested to override it, fade out the
+        // ripple more quickly.
+        if (mCode == KeyEvent.KEYCODE_HOME && Dependency.get(AssistManager.class)
+                .shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
+            mRipple.speedUpNextFade();
+        }
         setPressed(false);
         mRipple.abortDelayedRipple();
         mGestureAborted = true;
     }
 
+    /** Run when the ripple for this button is next invisible. Only used once. */
+    public void setOnRippleInvisibleRunnable(Runnable onRippleInvisibleRunnable) {
+        mRipple.setOnInvisibleRunnable(onRippleInvisibleRunnable);
+    }
+
     @Override
     public void setDarkIntensity(float darkIntensity) {
         mDarkIntensity = darkIntensity;
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 093d098..d9a8080 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -325,7 +325,13 @@
         } else {
             // TODO(b/278729185): Replace fire and forget service with a bounded service.
             val intent = NoteTaskControllerUpdateService.createIntent(context)
-            context.startServiceAsUser(intent, user)
+            try {
+                // If the user is stopped before 'startServiceAsUser' kicks-in, a
+                // 'SecurityException' will be thrown.
+                context.startServiceAsUser(intent, user)
+            } catch (e: SecurityException) {
+                debugLog(error = e) { "Unable to start 'NoteTaskControllerUpdateService'." }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
index 905d8ef..840db26 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
@@ -29,12 +29,18 @@
  * Provides a shortcut to start an activity from [QSTileUserActionInteractor]. It supports keyguard
  * dismissing and tile from-view animations.
  */
-@SysUISingleton
-class QSTileIntentUserInputHandler
-@Inject
-constructor(private val activityStarter: ActivityStarter) {
+interface QSTileIntentUserInputHandler {
 
-    fun handle(view: View?, intent: Intent) {
+    fun handle(view: View?, intent: Intent)
+    fun handle(view: View?, pendingIntent: PendingIntent)
+}
+
+@SysUISingleton
+class QSTileIntentUserInputHandlerImpl
+@Inject
+constructor(private val activityStarter: ActivityStarter) : QSTileIntentUserInputHandler {
+
+    override fun handle(view: View?, intent: Intent) {
         val animationController: ActivityLaunchAnimator.Controller? =
             view?.let {
                 ActivityLaunchAnimator.Controller.fromView(
@@ -46,7 +52,7 @@
     }
 
     // TODO(b/249804373): make sure to allow showing activities over the lockscreen. See b/292112939
-    fun handle(view: View?, pendingIntent: PendingIntent) {
+    override fun handle(view: View?, pendingIntent: PendingIntent) {
         if (!pendingIntent.isActivity) {
             return
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index 4dc1c82..2074a14 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.log.dagger.QSTilesLogBuffers
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import com.android.systemui.statusbar.StatusBarState
@@ -34,7 +33,7 @@
 class QSTileLogger
 @Inject
 constructor(
-    @QSTilesLogBuffers logBuffers: Map<TileSpec, LogBuffer>,
+    @QSTilesLogBuffers logBuffers: Map<String, LogBuffer>,
     private val factory: LogBufferFactory,
     private val mStatusBarStateController: StatusBarStateController,
 ) {
@@ -163,22 +162,15 @@
 
     private fun TileSpec.getLogBuffer(): LogBuffer =
         synchronized(logBufferCache) {
-            logBufferCache.getOrPut(this) {
+            logBufferCache.getOrPut(this.spec) {
                 factory.create(
-                    "QSTileLog_${this.getLogTag()}",
+                    this.getLogTag(),
                     BUFFER_MAX_SIZE /* maxSize */,
                     false /* systrace */
                 )
             }
         }
 
-    private fun DataUpdateTrigger.toLogString(): String =
-        when (this) {
-            is DataUpdateTrigger.ForceUpdate -> "force"
-            is DataUpdateTrigger.InitialRequest -> "init"
-            is DataUpdateTrigger.UserInput<*> -> input.action.toLogString()
-        }
-
     private fun QSTileUserAction.toLogString(): String =
         when (this) {
             is QSTileUserAction.Click -> "click"
@@ -198,7 +190,7 @@
             "]"
 
     private companion object {
-        const val TAG_FORMAT_PREFIX = "QSLog"
+        const val TAG_FORMAT_PREFIX = "QSLog_tile_"
         const val DATA_MAX_LENGTH = 50
         const val BUFFER_MAX_SIZE = 25
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 0bee48f..12a083e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -39,7 +39,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -47,6 +46,8 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.cancellable
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOn
@@ -57,6 +58,7 @@
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * Provides a hassle-free way to implement new tiles according to current System UI architecture
@@ -83,10 +85,8 @@
 
     private val users: MutableStateFlow<UserHandle> =
         MutableStateFlow(userRepository.getSelectedUserInfo().userHandle)
-    private val userInputs: MutableSharedFlow<QSTileUserAction> =
-        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
-    private val forceUpdates: MutableSharedFlow<Unit> =
-        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+    private val userInputs: MutableSharedFlow<QSTileUserAction> = MutableSharedFlow()
+    private val forceUpdates: MutableSharedFlow<Unit> = MutableSharedFlow()
     private val spec
         get() = config.tileSpec
 
@@ -130,7 +130,7 @@
             tileData.replayCache.isNotEmpty(),
             state.replayCache.isNotEmpty()
         )
-        userInputs.tryEmit(userAction)
+        tileScope.launch { userInputs.emit(userAction) }
     }
 
     override fun destroy() {
@@ -151,11 +151,16 @@
                             emit(DataUpdateTrigger.InitialRequest)
                             qsTileLogger.logInitialRequest(spec)
                         }
+                        .shareIn(tileScope, SharingStarted.WhileSubscribed())
                 tileDataInteractor()
                     .tileData(user, updateTriggers)
+                    // combine makes sure updateTriggers is always listened even if
+                    // tileDataInteractor#tileData doesn't flatMapLatest on it
+                    .combine(updateTriggers) { data, _ -> data }
                     .cancellable()
                     .flowOn(backgroundDispatcher)
             }
+            .distinctUntilChanged()
             .shareIn(
                 tileScope,
                 SharingStarted.WhileSubscribed(),
@@ -171,8 +176,8 @@
      *
      * Subscribing to the result flow twice will result in doubling all actions, logs and analytics.
      */
-    private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> {
-        return userInputs
+    private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> =
+        userInputs
             .filterFalseActions()
             .filterByPolicy(user)
             .throttle(CLICK_THROTTLE_DURATION, systemClock)
@@ -187,7 +192,6 @@
             }
             .onEach { userActionInteractor().handleInput(it.input) }
             .flowOn(backgroundDispatcher)
-    }
 
     private fun Flow<QSTileUserAction>.filterByPolicy(user: UserHandle): Flow<QSTileUserAction> =
         config.policy.let { policy ->
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
index 32522ad..94137c8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.qs.tiles.di
 
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerImpl
 import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
@@ -45,4 +47,9 @@
     @Multibinds fun tileViewModelMap(): Map<String, QSTileViewModel>
 
     @Binds fun bindQSTileConfigProvider(impl: QSTileConfigProviderImpl): QSTileConfigProvider
+
+    @Binds
+    fun bindQSTileIntentUserInputHandler(
+        impl: QSTileIntentUserInputHandlerImpl
+    ): QSTileIntentUserInputHandler
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index c4d7dfb..18a4e2d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -36,9 +36,9 @@
  */
 sealed interface QSTileUIConfig {
 
-    val tileIconRes: Int
+    val iconRes: Int
         @DrawableRes get
-    val tileLabelRes: Int
+    val labelRes: Int
         @StringRes get
 
     /**
@@ -46,16 +46,16 @@
      * of [Resource]. Returns [Resources.ID_NULL] for each field.
      */
     data object Empty : QSTileUIConfig {
-        override val tileIconRes: Int
+        override val iconRes: Int
             get() = Resources.ID_NULL
-        override val tileLabelRes: Int
+        override val labelRes: Int
             get() = Resources.ID_NULL
     }
 
     /** Config containing actual icon and label resources. */
     data class Resource(
-        @StringRes override val tileIconRes: Int,
-        @StringRes override val tileLabelRes: Int,
+        @DrawableRes override val iconRes: Int,
+        @StringRes override val labelRes: Int,
     ) : QSTileUIConfig
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
index dc5cccc..30b87cc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.viewmodel
 
+import android.content.Context
 import android.service.quicksettings.Tile
 import com.android.systemui.common.shared.model.Icon
 
@@ -41,11 +42,19 @@
 
     companion object {
 
+        fun build(
+            context: Context,
+            config: QSTileUIConfig,
+            build: Builder.() -> Unit
+        ): QSTileState =
+            build(
+                { Icon.Resource(config.iconRes, null) },
+                context.getString(config.labelRes),
+                build,
+            )
+
         fun build(icon: () -> Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState =
             Builder(icon, label).apply(build).build()
-
-        fun build(icon: Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState =
-            build({ icon }, label, build)
     }
 
     enum class ActivationState(val legacyState: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index efa6da7..771d07c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -192,7 +192,7 @@
         with(qsTileViewModel.config.uiConfig) {
             when (this) {
                 is QSTileUIConfig.Empty -> qsTileViewModel.currentState?.label ?: ""
-                is QSTileUIConfig.Resource -> context.getString(tileLabelRes)
+                is QSTileUIConfig.Resource -> context.getString(labelRes)
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 1334660..377803f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -85,8 +85,10 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBar;
 import com.android.systemui.navigationbar.NavigationBarController;
@@ -102,6 +104,7 @@
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
@@ -109,8 +112,6 @@
 import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
 import com.android.wm.shell.sysui.ShellInterface;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -122,6 +123,8 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import dagger.Lazy;
+
 /**
  * Class to send information from overview to launcher with a binder.
  */
@@ -160,7 +163,7 @@
     private final ScreenshotHelper mScreenshotHelper;
     private final CommandQueue mCommandQueue;
     private final UserTracker mUserTracker;
-    private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
+    private final ISysuiUnlockAnimationController mSysuiUnlockAnimationController;
     private final Optional<UnfoldTransitionProgressForwarder> mUnfoldTransitionProgressForwarder;
     private final UiEventLogger mUiEventLogger;
     private final DisplayTracker mDisplayTracker;
@@ -580,6 +583,7 @@
             UiEventLogger uiEventLogger,
             DisplayTracker displayTracker,
             KeyguardUnlockAnimationController sysuiUnlockAnimationController,
+            InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager,
             AssistUtils assistUtils,
             FeatureFlags featureFlags,
             SceneContainerFlags sceneContainerFlags,
@@ -613,7 +617,12 @@
         mUiEventLogger = uiEventLogger;
         mDisplayTracker = displayTracker;
         mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder;
-        mSysuiUnlockAnimationController = sysuiUnlockAnimationController;
+
+        if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+            mSysuiUnlockAnimationController = sysuiUnlockAnimationController;
+        } else {
+            mSysuiUnlockAnimationController = inWindowLauncherUnlockAnimationManager;
+        }
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 018f31b..e40d2b7 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.scene.shared.flag
 
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.Flags.sceneContainer
 import com.android.systemui.compose.ComposeFacade
@@ -57,8 +58,6 @@
         @VisibleForTesting
         val classicFlagTokens: List<Flag<Boolean>> =
             listOf(
-                Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA,
-                Flags.MIGRATE_LOCK_ICON,
                 Flags.MIGRATE_NSSL,
                 Flags.MIGRATE_KEYGUARD_STATUS_VIEW,
                 Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW,
@@ -72,6 +71,10 @@
                 flagName = AConfigFlags.FLAG_SCENE_CONTAINER,
                 flagValue = sceneContainer(),
             ),
+            AconfigFlagMustBeEnabled(
+                flagName = AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
+                flagValue = keyguardBottomAreaRefactor(),
+            ),
         ) +
             classicFlagTokens.map { flagToken -> FlagMustBeEnabled(flagToken) } +
             listOf(ComposeMustBeAvailable(), CompileTimeFlagMustBeEnabled())
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index 6fa592c..b30bc56 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.settings.brightness;
 
-import static com.android.systemui.flags.Flags.HAPTIC_BRIGHTNESS_SLIDER;
+import static com.android.systemui.Flags.hapticBrightnessSlider;
 
 import android.content.Context;
 import android.view.LayoutInflater;
@@ -32,7 +32,6 @@
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.haptics.slider.SeekableSliderEventProducer;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -280,7 +279,6 @@
 
         private final FalsingManager mFalsingManager;
         private final UiEventLogger mUiEventLogger;
-        private final FeatureFlagsClassic mFeatureFlags;
         private final VibratorHelper mVibratorHelper;
         private final SystemClock mSystemClock;
         private final CoroutineDispatcher mMainDispatcher;
@@ -292,12 +290,10 @@
                 UiEventLogger uiEventLogger,
                 VibratorHelper vibratorHelper,
                 SystemClock clock,
-                FeatureFlagsClassic featureFlags,
                 @Main CoroutineDispatcher mainDispatcher,
                 ActivityStarter activityStarter) {
             mFalsingManager = falsingManager;
             mUiEventLogger = uiEventLogger;
-            mFeatureFlags = featureFlags;
             mVibratorHelper = vibratorHelper;
             mSystemClock = clock;
             mMainDispatcher = mainDispatcher;
@@ -320,7 +316,7 @@
             root.setActivityStarter(mActivityStarter);
 
             BrightnessSliderHapticPlugin plugin;
-            if (mFeatureFlags.isEnabled(HAPTIC_BRIGHTNESS_SLIDER)) {
+            if (hapticBrightnessSlider()) {
                 plugin = new BrightnessSliderHapticPluginImpl(
                     mVibratorHelper,
                     mSystemClock,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 85a4a7e..823caa0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -24,6 +24,7 @@
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
+import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
@@ -1070,7 +1071,7 @@
         mQsController.init();
         mShadeHeadsUpTracker.addTrackingHeadsUpListener(
                 mNotificationStackScrollLayoutController::setTrackingHeadsUp);
-        if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (!keyguardBottomAreaRefactor()) {
             setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
         }
 
@@ -1409,7 +1410,7 @@
 
         updateViewControllers(userAvatarView, keyguardUserSwitcherView);
 
-        if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (!keyguardBottomAreaRefactor()) {
             // Update keyguard bottom area
             int index = mView.indexOfChild(mKeyguardBottomArea);
             mView.removeView(mKeyguardBottomArea);
@@ -1443,7 +1444,7 @@
                     mBarState);
         }
 
-        if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (!keyguardBottomAreaRefactor()) {
             setKeyguardBottomAreaVisibility(mBarState, false);
         }
 
@@ -1456,7 +1457,7 @@
     }
 
     private void initBottomArea() {
-        if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (!keyguardBottomAreaRefactor()) {
             mKeyguardBottomArea.init(
                 mKeyguardBottomAreaViewModel,
                 mFalsingManager,
@@ -1643,7 +1644,7 @@
             mKeyguardStatusViewController.setLockscreenClockY(
                     mClockPositionAlgorithm.getExpandedPreferredClockY());
         }
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             mKeyguardInteractor.setClockPosition(
                 mClockPositionResult.clockX, mClockPositionResult.clockY);
         } else {
@@ -2710,7 +2711,7 @@
 
         float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction());
         alpha *= mBottomAreaShadeAlpha;
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             mKeyguardInteractor.setAlpha(alpha);
         } else {
             mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -2936,7 +2937,7 @@
     }
 
     private void updateDozingVisibilities(boolean animate) {
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             mKeyguardInteractor.setAnimateDozingTransitions(animate);
         } else {
             mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -3144,7 +3145,7 @@
         mDozing = dozing;
         // TODO (b/) make listeners for this
         mNotificationStackScrollLayoutController.setDozing(mDozing, animate);
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+        if (keyguardBottomAreaRefactor()) {
             mKeyguardInteractor.setAnimateDozingTransitions(animate);
         } else {
             mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -4441,7 +4442,7 @@
                     goingToFullShade,
                     mBarState);
 
-            if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+            if (!keyguardBottomAreaRefactor()) {
                 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
             }
 
@@ -4698,7 +4699,7 @@
             mKeyguardStatusViewController.setAlpha(alpha);
             stackScroller.setAlpha(alpha);
 
-            if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
+            if (keyguardBottomAreaRefactor()) {
                 mKeyguardInteractor.setAlpha(alpha);
             } else {
                 mKeyguardBottomAreaInteractor.setAlpha(alpha);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 121aa42..e9779cd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -52,6 +52,7 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -59,6 +60,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.ui.view.WindowRootViewComponent;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -150,6 +152,7 @@
             ConfigurationController configurationController,
             KeyguardViewMediator keyguardViewMediator,
             KeyguardBypassController keyguardBypassController,
+            @Main Executor mainExecutor,
             @Background Executor backgroundExecutor,
             SysuiColorExtractor colorExtractor,
             DumpManager dumpManager,
@@ -158,7 +161,8 @@
             AuthController authController,
             Lazy<ShadeInteractor> shadeInteractorLazy,
             ShadeWindowLogger logger,
-            Lazy<SelectedUserInteractor> userInteractor) {
+            Lazy<SelectedUserInteractor> userInteractor,
+            UserTracker userTracker) {
         mContext = context;
         mWindowRootViewComponentFactory = windowRootViewComponentFactory;
         mWindowManager = windowManager;
@@ -184,7 +188,9 @@
                 .addCallback(mStateListener,
                         SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER);
         configurationController.addCallback(this);
-
+        if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) {
+            userTracker.addCallback(mUserTrackerCallback, mainExecutor);
+        }
         float desiredPreferredRefreshRate = context.getResources()
                 .getInteger(R.integer.config_keyguardRefreshRate);
         float actualPreferredRefreshRate = -1;
@@ -572,6 +578,7 @@
                 state.qsExpanded,
                 state.headsUpNotificationShowing,
                 state.lightRevealScrimOpaque,
+                state.isSwitchingUsers,
                 state.forceWindowCollapsed,
                 state.forceDozeBrightness,
                 state.forceUserActivity,
@@ -624,7 +631,8 @@
     }
 
     private void applyHasTopUi(NotificationShadeWindowState state) {
-        mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state);
+        mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state)
+                || state.isSwitchingUsers;
     }
 
     private void applyNotTouchable(NotificationShadeWindowState state) {
@@ -954,4 +962,24 @@
             setDreaming(isDreaming);
         }
     };
+
+    private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() {
+        @Override
+        public void onBeforeUserSwitching(int newUser) {
+            setIsSwitchingUsers(true);
+        }
+
+        @Override
+        public void onUserChanged(int newUser, Context userContext) {
+            setIsSwitchingUsers(false);
+        }
+
+        private void setIsSwitchingUsers(boolean isSwitchingUsers) {
+            if (mCurrentState.isSwitchingUsers == isSwitchingUsers) {
+                return;
+            }
+            mCurrentState.isSwitchingUsers = isSwitchingUsers;
+            apply(mCurrentState);
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index fbe164a..0b20170 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -40,6 +40,7 @@
     @JvmField var qsExpanded: Boolean = false,
     @JvmField var headsUpNotificationShowing: Boolean = false,
     @JvmField var lightRevealScrimOpaque: Boolean = false,
+    @JvmField var isSwitchingUsers: Boolean = false,
     @JvmField var forceWindowCollapsed: Boolean = false,
     @JvmField var forceDozeBrightness: Boolean = false,
     // TODO: forceUserActivity seems to be unused, delete?
@@ -78,6 +79,7 @@
             qsExpanded.toString(),
             headsUpNotificationShowing.toString(),
             lightRevealScrimOpaque.toString(),
+            isSwitchingUsers.toString(),
             forceWindowCollapsed.toString(),
             forceDozeBrightness.toString(),
             forceUserActivity.toString(),
@@ -117,6 +119,7 @@
             qsExpanded: Boolean,
             headsUpShowing: Boolean,
             lightRevealScrimOpaque: Boolean,
+            isSwitchingUsers: Boolean,
             forceCollapsed: Boolean,
             forceDozeBrightness: Boolean,
             forceUserActivity: Boolean,
@@ -145,6 +148,7 @@
                 this.qsExpanded = qsExpanded
                 this.headsUpNotificationShowing = headsUpShowing
                 this.lightRevealScrimOpaque = lightRevealScrimOpaque
+                this.isSwitchingUsers = isSwitchingUsers
                 this.forceWindowCollapsed = forceCollapsed
                 this.forceDozeBrightness = forceDozeBrightness
                 this.forceUserActivity = forceUserActivity
@@ -191,6 +195,7 @@
                 "qsExpanded",
                 "headsUpShowing",
                 "lightRevealScrimOpaque",
+                "isSwitchingUsers",
                 "forceCollapsed",
                 "forceDozeBrightness",
                 "forceUserActivity",
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index db0fa99..65b798a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -251,7 +251,7 @@
 
             val replacingIcons =
                 iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, v) ->
-                    viewStore.iconView(v.notifKey)?.statusBarIcon
+                    viewStore.iconView(v.notifKey).statusBarIcon
                 }
             view.setReplacingIcons(replacingIcons)
 
@@ -264,7 +264,7 @@
                 .mapNotNull { key -> childrenByNotifKey[key] }
                 .forEach { child -> view.removeView(child) }
 
-            val toAdd = iconsDiff.added.mapNotNull { viewStore.iconView(it.notifKey) }
+            val toAdd = iconsDiff.added.map { viewStore.iconView(it.notifKey) }
             for ((i, sbiv) in toAdd.withIndex()) {
                 // The view might still be transiently added if it was just removed
                 // and added again
@@ -277,7 +277,7 @@
             val childCount = view.childCount
             for (i in 0 until childCount) {
                 val actual = view.getChildAt(i)
-                val expected = viewStore.iconView(iconsData.visibleKeys[i].notifKey)!!
+                val expected = viewStore.iconView(iconsData.visibleKeys[i].notifKey)
                 if (actual === expected) {
                     continue
                 }
@@ -314,7 +314,7 @@
 
     /** External storage for [StatusBarIconView] instances. */
     fun interface IconViewStore {
-        fun iconView(key: String): StatusBarIconView?
+        fun iconView(key: String): StatusBarIconView
     }
 
     @ColorInt private val DEFAULT_AOD_ICON_COLOR = Color.WHITE
@@ -326,8 +326,10 @@
 constructor(
     private val notifCollection: NotifCollection,
 ) : IconViewStore {
-    override fun iconView(key: String): StatusBarIconView? =
-        notifCollection.getEntry(key)?.icons?.shelfIcon
+    override fun iconView(key: String): StatusBarIconView {
+        val entry = notifCollection.getEntry(key) ?: error("No entry found for key: $key")
+        return entry.icons.shelfIcon ?: error("No shelf IconView found for key: $key")
+    }
 }
 
 /** [IconViewStore] for the always-on display. */
@@ -336,8 +338,10 @@
 constructor(
     private val notifCollection: NotifCollection,
 ) : IconViewStore {
-    override fun iconView(key: String): StatusBarIconView? =
-        notifCollection.getEntry(key)?.icons?.aodIcon
+    override fun iconView(key: String): StatusBarIconView {
+        val entry = notifCollection.getEntry(key) ?: error("No entry found for key: $key")
+        return entry.icons.aodIcon ?: error("No AOD IconView found for key: $key")
+    }
 }
 
 /** [IconViewStore] for the status bar. */
@@ -346,8 +350,10 @@
 constructor(
     private val notifCollection: NotifCollection,
 ) : IconViewStore {
-    override fun iconView(key: String): StatusBarIconView? =
-        notifCollection.getEntry(key)?.icons?.statusBarIcon
+    override fun iconView(key: String): StatusBarIconView {
+        val entry = notifCollection.getEntry(key) ?: error("No entry found for key: $key")
+        return entry.icons.statusBarIcon ?: error("No status bar IconView found for key: $key")
+    }
 }
 
 private val View.viewBounds: Rect
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 966ff35..ec90a8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -90,10 +90,10 @@
                 } else {
                     setWillBeGone(true);
                 }
-                setContentVisible(visible, true /* animate */, null /* runAfter */);
+                setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
             } else {
                 setVisibility(visible ? VISIBLE : GONE);
-                setContentVisible(visible, false /* animate */, null /* runAfter */);
+                setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
                 setWillBeGone(false);
                 notifyHeightChanged(false /* needsAnimation */);
             }
@@ -108,7 +108,7 @@
      * Change content visibility to {@code visible}, animated.
      */
     public void setContentVisibleAnimated(boolean visible) {
-        setContentVisible(visible, true /* animate */, null /* runAfter */);
+        setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt
new file mode 100644
index 0000000..44387c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notifications live data store refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationsLiveDataStoreRefactor {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsLiveDataStoreRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
index 5c1149b..580431a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -53,7 +53,7 @@
         mContents = requireViewById(R.id.content);
         bindContents();
         super.onFinishInflate();
-        setVisible(true /* nowVisible */, false /* animate */);
+        setVisible(true /* visible */, false /* animate */);
     }
 
     private void bindContents() {
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
index 771a8c8..799e5af 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/LockscreenFragment.java
@@ -48,6 +48,8 @@
 import com.android.systemui.statusbar.policy.ExtensionController.TunerFactory;
 import com.android.systemui.tuner.ShortcutParser.Shortcut;
 import com.android.systemui.tuner.TunerService.Tunable;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
 
 import java.util.ArrayList;
 import java.util.Map;
@@ -69,6 +71,9 @@
     private TunerService mTunerService;
     private Handler mHandler;
 
+    // aapt doesn't generate keep rules for android:fragment references in <Preference> tags, so
+    // explicitly declare references per usage in `R.xml.lockscreen_settings`. See b/120445169.
+    @UsesReflection(@KeepTarget(classConstant = ShortcutPicker.class))
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         mTunerService = Dependency.get(TunerService.class);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/OtherPrefs.java b/packages/SystemUI/src/com/android/systemui/tuner/OtherPrefs.java
index 32b1b26..8d85999 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/OtherPrefs.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/OtherPrefs.java
@@ -19,8 +19,13 @@
 import androidx.preference.PreferenceFragment;
 
 import com.android.systemui.res.R;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
 
 public class OtherPrefs extends PreferenceFragment {
+    // aapt doesn't generate keep rules for android:fragment references in <Preference> tags, so
+    // explicitly declare references per usage in `R.xml.other_settings`. See b/120445169.
+    @UsesReflection(@KeepTarget(classConstant = PowerNotificationControlsFragment.class))
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         addPreferencesFromResource(R.xml.other_settings);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java
index 9cc526a..873b6d5 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java
@@ -35,6 +35,8 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.plugins.PluginPrefs;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
 
 public class TunerFragment extends PreferenceFragment {
 
@@ -77,6 +79,13 @@
         getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
     }
 
+    // aapt doesn't generate keep rules for android:fragment references in <Preference> tags, so
+    // explicitly declare references per usage in `R.xml.tuner_prefs`. See b/120445169.
+    @UsesReflection({
+        @KeepTarget(classConstant = LockscreenFragment.class),
+        @KeepTarget(classConstant = NavBarTuner.class),
+        @KeepTarget(classConstant = PluginFragment.class),
+    })
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         addPreferencesFromResource(R.xml.tuner_prefs);
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
index 0cb913b..fd50f15 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shared.system.ActivityManagerWrapper
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationListener
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -66,6 +67,7 @@
 @Module(includes = [TestMocksModule.Bindings::class])
 data class TestMocksModule(
     @get:Provides val activityStarter: ActivityStarter = mock(),
+    @get:Provides val activityManagerWrapper: ActivityManagerWrapper = mock(),
     @get:Provides val ambientState: AmbientState = mock(),
     @get:Provides val bubbles: Optional<Bubbles> = Optional.of(mock()),
     @get:Provides val darkIconDispatcher: DarkIconDispatcher = mock(),
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index aabe633..a38ba00 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
+import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.plugins.ClockAnimations;
@@ -204,7 +205,8 @@
                 mock(AlwaysOnDisplayNotificationIconViewStore.class),
                 KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(),
                 mKeyguardClockInteractor,
-                mFakeFeatureFlags
+                mFakeFeatureFlags,
+                mock(InWindowLauncherUnlockAnimationManager.class)
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index d61ca69..1d4f2cb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -21,7 +21,6 @@
 import static com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
-import static com.android.systemui.flags.Flags.MIGRATE_LOCK_ICON;
 
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
@@ -39,6 +38,7 @@
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.AuthRippleController;
@@ -148,9 +148,10 @@
         when(mStatusBarStateController.isDozing()).thenReturn(false);
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
 
+        mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR);
+
         mFeatureFlags = new FakeFeatureFlags();
         mFeatureFlags.set(FACE_AUTH_REFACTOR, false);
-        mFeatureFlags.set(MIGRATE_LOCK_ICON, false);
         mFeatureFlags.set(LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false);
         mFeatureFlags.set(LOCKSCREEN_ENABLE_LANDSCAPE, false);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimatorTest.java
index e3a2c59..d77a80a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/RadiiAnimatorTest.java
@@ -31,42 +31,60 @@
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Tests for {@link RadiiAnimator}. */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class RadiiAnimatorTest extends SysuiTestCase {
     float[] mResultRadii = new float[RadiiAnimator.RADII_COUNT];
+    final AtomicBoolean mAnimationStarted = new AtomicBoolean(false);
+    final AtomicBoolean mAnimationStopped = new AtomicBoolean(false);
+    final IRadiiAnimationListener mRadiiAnimationListener = new IRadiiAnimationListener() {
+        @Override
+        public void onRadiiAnimationUpdate(float[] radii) {
+            mResultRadii = radii;
+        }
+
+        @Override
+        public void onRadiiAnimationStart() {
+            mAnimationStarted.set(true);
+        }
+
+        @Override
+        public void onRadiiAnimationStop() {
+            mAnimationStopped.set(true);
+        }
+    };
 
     @Test
     public void constructor() {
         final float[] radii = generateRadii(0.0f);
-        final RadiiAnimator radiiAnimator = new RadiiAnimator(radii, newRadii -> {});
-
+        final RadiiAnimator radiiAnimator = new RadiiAnimator(radii, mRadiiAnimationListener);
         assertThat(radiiAnimator.evaluate(0.0f)).isEqualTo(radii);
     }
 
     @Test
-    public void skip_updates_to_end() {
+    public void skipAnimation_updatesToEnd() {
         final float[] startRadii = generateRadii(0.0f);
         final float[] endRadii = generateRadii(1.0f);
 
         final RadiiAnimator radiiAnimator = setupAnimator(startRadii);
 
+        mAnimationStarted.set(false);
+        mAnimationStopped.set(false);
         new Handler(Looper.getMainLooper()).post(() -> radiiAnimator.startAnimation(endRadii));
-        TestUtils.waitForCondition(radiiAnimator::isStarted, "Animation did not start.");
+        TestUtils.waitForCondition(mAnimationStarted::get, "Animation did not start.");
         TestUtils.waitForCondition(() -> Arrays.equals(radiiAnimator.evaluate(0.0f), startRadii)
-                                && Arrays.equals(radiiAnimator.evaluate(1.0f), endRadii),
+                        && Arrays.equals(radiiAnimator.evaluate(1.0f), endRadii),
                 "Animator did not initialize to start and end values");
-
         new Handler(Looper.getMainLooper()).post(radiiAnimator::skipAnimationToEnd);
-        TestUtils.waitForCondition(
-                () -> !radiiAnimator.isStarted(), "Animation did not end.");
+        TestUtils.waitForCondition(mAnimationStopped::get, "Animation did not stop.");
         assertThat(mResultRadii).usingTolerance(0.001).containsExactly(endRadii);
     }
 
     @Test
-    public void animation_can_repeat() {
+    public void finishedAnimation_canRepeat() {
         final float[] startRadii = generateRadii(0.0f);
         final float[] midRadii = generateRadii(1.0f);
         final float[] endRadii = generateRadii(2.0f);
@@ -88,15 +106,15 @@
 
     private RadiiAnimator setupAnimator(float[] startRadii) {
         mResultRadii = new float[RadiiAnimator.RADII_COUNT];
-        return new RadiiAnimator(startRadii,
-                newRadii -> mResultRadii = newRadii);
+        return new RadiiAnimator(startRadii, mRadiiAnimationListener);
     }
 
     private void playAndSkipAnimation(RadiiAnimator animator, float[] endRadii) {
+        mAnimationStarted.set(false);
+        mAnimationStopped.set(false);
         new Handler(Looper.getMainLooper()).post(() -> animator.startAnimation(endRadii));
-        TestUtils.waitForCondition(animator::isStarted, "Animation did not start.");
+        TestUtils.waitForCondition(mAnimationStarted::get, "Animation did not start.");
         new Handler(Looper.getMainLooper()).post(animator::skipAnimationToEnd);
-        TestUtils.waitForCondition(
-                () -> !animator.isStarted(), "Animation did not end.");
+        TestUtils.waitForCondition(mAnimationStopped::get, "Animation did not stop.");
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index ef06e0e..b4b02a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -52,7 +52,6 @@
 import androidx.test.filters.SmallTest
 import com.airbnb.lottie.LottieAnimationView
 import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.SysuiTestableContext
 import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
@@ -64,6 +63,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
@@ -111,6 +111,7 @@
     @Mock lateinit var displayManager: DisplayManager
     @Mock lateinit var handler: Handler
     @Mock lateinit var dumpManager: DumpManager
+    @Mock lateinit var fpsUnlockTracker: FpsUnlockTracker
     @Captor lateinit var overlayCaptor: ArgumentCaptor<View>
     @Captor lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
@@ -269,7 +270,8 @@
                 handler,
                 alternateBouncerInteractor,
                 TestCoroutineScope(),
-                dumpManager
+                dumpManager,
+                fpsUnlockTracker
             )
         displayStateRepository.setIsInRearDisplayMode(inRearDisplayMode)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index cbde78b..675ca63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -235,6 +235,8 @@
     private InputManager mInputManager;
     @Mock
     private ViewRootImpl mViewRootImpl;
+    @Mock
+    private FpsUnlockTracker mFpsUnlockTracker;
 
     @Before
     public void setUp() {
@@ -326,7 +328,8 @@
                 mock(KeyguardFaceAuthInteractor.class),
                 mUdfpsKeyguardAccessibilityDelegate,
                 mUdfpsKeyguardViewModels,
-                mSelectedUserInteractor
+                mSelectedUserInteractor,
+                mFpsUnlockTracker
         );
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
new file mode 100644
index 0000000..14ec4d4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.data.db
+
+import android.content.ComponentName
+import androidx.room.Room
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.google.common.truth.Truth.assertThat
+import java.io.IOException
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalWidgetDaoTest : SysuiTestCase() {
+    @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule()
+
+    private lateinit var db: CommunalDatabase
+    private lateinit var communalWidgetDao: CommunalWidgetDao
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    @Before
+    @Throws(IOException::class)
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        db =
+            Room.inMemoryDatabaseBuilder(context, CommunalDatabase::class.java)
+                .allowMainThreadQueries()
+                .build()
+        communalWidgetDao = db.communalWidgetDao()
+    }
+
+    @After
+    @Throws(IOException::class)
+    fun teardown() {
+        db.close()
+    }
+
+    @Test
+    fun addWidget_readValueInDb() =
+        testScope.runTest {
+            val (widgetId, provider, priority) = widgetInfo1
+            communalWidgetDao.addWidget(
+                widgetId = widgetId,
+                provider = provider,
+                priority = priority,
+            )
+            val entry = communalWidgetDao.getWidgetByIdNow(id = 1)
+            assertThat(entry).isEqualTo(communalWidgetItemEntry1)
+        }
+
+    @Test
+    fun addWidget_emitsActiveWidgetsInDb(): Unit =
+        testScope.runTest {
+            val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
+            val widgets = collectLastValue(communalWidgetDao.getWidgets())
+            widgetsToAdd.forEach {
+                val (widgetId, provider, priority) = it
+                communalWidgetDao.addWidget(
+                    widgetId = widgetId,
+                    provider = provider,
+                    priority = priority,
+                )
+            }
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry1,
+                    communalWidgetItemEntry1,
+                    communalItemRankEntry2,
+                    communalWidgetItemEntry2
+                )
+        }
+
+    @Test
+    fun deleteWidget_emitsActiveWidgetsInDb() =
+        testScope.runTest {
+            val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
+            val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+            widgetsToAdd.forEach {
+                val (widgetId, provider, priority) = it
+                communalWidgetDao.addWidget(
+                    widgetId = widgetId,
+                    provider = provider,
+                    priority = priority,
+                )
+            }
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry1,
+                    communalWidgetItemEntry1,
+                    communalItemRankEntry2,
+                    communalWidgetItemEntry2
+                )
+
+            communalWidgetDao.deleteWidgetById(communalWidgetItemEntry1.widgetId)
+            assertThat(widgets()).containsExactly(communalItemRankEntry2, communalWidgetItemEntry2)
+        }
+
+    data class FakeWidgetMetadata(
+        val widgetId: Int,
+        val provider: ComponentName,
+        val priority: Int
+    )
+
+    companion object {
+        val widgetInfo1 =
+            FakeWidgetMetadata(
+                widgetId = 1,
+                provider = ComponentName("pk_name", "cls_name_1"),
+                priority = 1
+            )
+        val widgetInfo2 =
+            FakeWidgetMetadata(
+                widgetId = 2,
+                provider = ComponentName("pk_name", "cls_name_2"),
+                priority = 2
+            )
+        val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority)
+        val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority)
+        val communalWidgetItemEntry1 =
+            CommunalWidgetItem(
+                uid = 1L,
+                widgetId = widgetInfo1.widgetId,
+                componentName = widgetInfo1.provider.flattenToString(),
+                itemId = communalItemRankEntry1.uid,
+            )
+        val communalWidgetItemEntry2 =
+            CommunalWidgetItem(
+                uid = 2L,
+                widgetId = widgetInfo2.widgetId,
+                componentName = widgetInfo2.provider.flattenToString(),
+                itemId = communalItemRankEntry2.uid,
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index fcb191b..ca8316d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -1,9 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.communal.data.repository
 
 import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.content.BroadcastReceiver
+import android.content.ComponentName
 import android.content.pm.PackageManager
 import android.os.UserHandle
 import android.os.UserManager
@@ -11,8 +28,10 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.communal.data.model.CommunalWidgetMetadata
-import com.android.systemui.communal.shared.model.CommunalContentSize
+import com.android.systemui.communal.data.db.CommunalItemRank
+import com.android.systemui.communal.data.db.CommunalWidgetDao
+import com.android.systemui.communal.data.db.CommunalWidgetItem
+import com.android.systemui.communal.shared.CommunalWidgetHost
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FeatureFlagsClassic
@@ -28,6 +47,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -66,9 +86,9 @@
 
     @Mock private lateinit var providerInfoA: AppWidgetProviderInfo
 
-    @Mock private lateinit var providerInfoB: AppWidgetProviderInfo
+    @Mock private lateinit var communalWidgetHost: CommunalWidgetHost
 
-    @Mock private lateinit var providerInfoC: AppWidgetProviderInfo
+    @Mock private lateinit var communalWidgetDao: CommunalWidgetDao
 
     private lateinit var communalRepository: FakeCommunalRepository
 
@@ -103,6 +123,92 @@
     }
 
     @Test
+    fun neverQueryDbForWidgets_whenFeatureIsDisabled() =
+        testScope.runTest {
+            communalEnabled(false)
+            val repository = initCommunalWidgetRepository()
+            collectLastValue(repository.communalWidgets)()
+            runCurrent()
+
+            verify(communalWidgetDao, Mockito.never()).getWidgets()
+        }
+
+    @Test
+    fun neverQueryDbForWidgets_whenFeatureEnabled_andUserLocked() =
+        testScope.runTest {
+            userUnlocked(false)
+            val repository = initCommunalWidgetRepository()
+            collectLastValue(repository.communalWidgets)()
+            runCurrent()
+
+            verify(communalWidgetDao, Mockito.never()).getWidgets()
+        }
+
+    @Test
+    fun communalWidgets_whenUserUnlocked_queryWidgetsFromDb() =
+        testScope.runTest {
+            userUnlocked(false)
+            val repository = initCommunalWidgetRepository()
+            val communalWidgets = collectLastValue(repository.communalWidgets)
+            communalWidgets()
+            runCurrent()
+            val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
+            val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
+            whenever(communalWidgetDao.getWidgets())
+                .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry)))
+            whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)
+
+            userUnlocked(true)
+            installedProviders(listOf(stopwatchProviderInfo))
+            broadcastReceiverUpdate()
+            runCurrent()
+
+            verify(communalWidgetDao).getWidgets()
+            assertThat(communalWidgets())
+                .containsExactly(
+                    CommunalWidgetContentModel(
+                        appWidgetId = communalWidgetItemEntry.widgetId,
+                        providerInfo = providerInfoA,
+                        priority = communalItemRankEntry.rank,
+                    )
+                )
+        }
+
+    @Test
+    fun addWidget_allocateId_bindWidget_andAddToDb() =
+        testScope.runTest {
+            userUnlocked(true)
+            val repository = initCommunalWidgetRepository()
+            runCurrent()
+
+            val provider = ComponentName("pkg_name", "cls_name")
+            val id = 1
+            val priority = 1
+            whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>()))
+                .thenReturn(id)
+            repository.addWidget(provider, priority)
+            runCurrent()
+
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider)
+            verify(communalWidgetDao).addWidget(id, provider, priority)
+        }
+
+    @Test
+    fun deleteWidget_removeWidgetId_andDeleteFromDb() =
+        testScope.runTest {
+            userUnlocked(true)
+            val repository = initCommunalWidgetRepository()
+            runCurrent()
+
+            val id = 1
+            repository.deleteWidget(id)
+            runCurrent()
+
+            verify(communalWidgetDao).deleteWidgetById(id)
+            verify(appWidgetHost).deleteAppWidgetId(id)
+        }
+
+    @Test
     fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() =
         testScope.runTest {
             communalEnabled(false)
@@ -183,34 +289,6 @@
         }
 
     @Test
-    fun appWidgetId_userLockedAgainAfterProviderInfoAvailable_deleteAppWidgetId() =
-        testScope.runTest {
-            whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(123456)
-            userUnlocked(false)
-            val repository = initCommunalWidgetRepository()
-            val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo)
-            assertThat(lastStopwatchProviderInfo()).isNull()
-
-            // User unlocks
-            userUnlocked(true)
-            installedProviders(listOf(stopwatchProviderInfo))
-            broadcastReceiverUpdate()
-
-            // Verify app widget id allocated
-            assertThat(lastStopwatchProviderInfo()?.appWidgetId).isEqualTo(123456)
-            verify(appWidgetHost).allocateAppWidgetId()
-            verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt())
-
-            // User locked again
-            userUnlocked(false)
-            broadcastReceiverUpdate()
-
-            // Verify app widget id deleted
-            assertThat(lastStopwatchProviderInfo()).isNull()
-            verify(appWidgetHost).deleteAppWidgetId(123456)
-        }
-
-    @Test
     fun appWidgetHost_userUnlocked_startListening() =
         testScope.runTest {
             userUnlocked(false)
@@ -246,95 +324,16 @@
             verify(appWidgetHost).stopListening()
         }
 
-    @Test
-    fun getCommunalWidgetAllowList_onInit() {
-        testScope.runTest {
-            val repository = initCommunalWidgetRepository()
-            val communalWidgetAllowlist = repository.communalWidgetAllowlist
-            assertThat(
-                    listOf(
-                        CommunalWidgetMetadata(
-                            componentName = fakeAllowlist[0],
-                            priority = 3,
-                            sizes = listOf(CommunalContentSize.HALF),
-                        ),
-                        CommunalWidgetMetadata(
-                            componentName = fakeAllowlist[1],
-                            priority = 2,
-                            sizes = listOf(CommunalContentSize.HALF),
-                        ),
-                        CommunalWidgetMetadata(
-                            componentName = fakeAllowlist[2],
-                            priority = 1,
-                            sizes = listOf(CommunalContentSize.HALF),
-                        ),
-                    )
-                )
-                .containsExactly(*communalWidgetAllowlist.toTypedArray())
-        }
-    }
-
-    // This behavior is temporary before the local database is set up.
-    @Test
-    fun communalWidgets_withPreviouslyBoundWidgets_removeEachBinding() =
-        testScope.runTest {
-            whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(1, 2, 3)
-            setAppWidgetIds(listOf(1, 2, 3))
-            whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)
-            userUnlocked(true)
-
-            val repository = initCommunalWidgetRepository()
-
-            collectLastValue(repository.communalWidgets)()
-
-            verify(appWidgetHost).deleteAppWidgetId(1)
-            verify(appWidgetHost).deleteAppWidgetId(2)
-            verify(appWidgetHost).deleteAppWidgetId(3)
-        }
-
-    @Test
-    fun communalWidgets_allowlistNotEmpty_bindEachWidgetFromTheAllowlist() =
-        testScope.runTest {
-            whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(0, 1, 2)
-            userUnlocked(true)
-
-            whenever(appWidgetManager.getAppWidgetInfo(0)).thenReturn(providerInfoA)
-            whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfoB)
-            whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfoC)
-
-            val repository = initCommunalWidgetRepository()
-
-            val inventory by collectLastValue(repository.communalWidgets)
-
-            assertThat(
-                    listOf(
-                        CommunalWidgetContentModel(
-                            appWidgetId = 0,
-                            providerInfo = providerInfoA,
-                            priority = 3,
-                        ),
-                        CommunalWidgetContentModel(
-                            appWidgetId = 1,
-                            providerInfo = providerInfoB,
-                            priority = 2,
-                        ),
-                        CommunalWidgetContentModel(
-                            appWidgetId = 2,
-                            providerInfo = providerInfoC,
-                            priority = 1,
-                        ),
-                    )
-                )
-                .containsExactly(*inventory!!.toTypedArray())
-        }
-
     private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl {
         return CommunalWidgetRepositoryImpl(
-            context,
             appWidgetManager,
             appWidgetHost,
+            testScope.backgroundScope,
+            testDispatcher,
             broadcastDispatcher,
             communalRepository,
+            communalWidgetHost,
+            communalWidgetDao,
             packageManager,
             userManager,
             userTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
index dcc15ae..b25fb6e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/ui/view/MirroringConfirmationDialogTest.kt
@@ -20,8 +20,8 @@
 import android.testing.TestableLooper
 import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import org.junit.After
@@ -45,7 +45,13 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        dialog = MirroringConfirmationDialog(context, onStartMirroringCallback, onCancelCallback)
+        dialog =
+            MirroringConfirmationDialog(
+                context,
+                onStartMirroringCallback,
+                onCancelCallback,
+                navbarBottomInsetsProvider = { 0 },
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt
index b589a2a..a903d25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt
@@ -22,6 +22,7 @@
 import android.content.res.Resources
 import android.content.res.Resources.NotFoundException
 import android.test.suitebuilder.annotation.SmallTest
+import com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -72,6 +73,8 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        mSetFlagsRule.disableFlags(FLAG_SYSUI_TEAMFOOD)
+
         flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA)
         flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB)
         mFeatureFlagsClassicDebug =
@@ -130,7 +133,7 @@
 
     @Test
     fun teamFoodFlag_True() {
-        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD)
+        mSetFlagsRule.enableFlags(FLAG_SYSUI_TEAMFOOD)
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isTrue()
 
@@ -145,7 +148,7 @@
             .thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any()))
             .thenReturn(false)
-        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD)
+        mSetFlagsRule.enableFlags(FLAG_SYSUI_TEAMFOOD)
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isFalse()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt
new file mode 100644
index 0000000..8b572eb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyboard.backlight.ui.view
+
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWithLooper
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyboardBacklightDialogTest : SysuiTestCase() {
+
+    private lateinit var dialog: KeyboardBacklightDialog
+    private lateinit var rootView: View
+    private val descriptionString = context.getString(R.string.keyboard_backlight_value)
+
+    @Before
+    fun setUp() {
+        dialog =
+            KeyboardBacklightDialog(context, initialCurrentLevel = 0, initialMaxLevel = MAX_LEVEL)
+        dialog.show()
+        rootView = dialog.requireViewById(R.id.keyboard_backlight_dialog_container)
+    }
+
+    @Test
+    fun rootViewContentDescription_containsInitialLevel() {
+        assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(INITIAL_LEVEL))
+    }
+
+    @Test
+    fun contentDescriptionUpdated_afterEveryLevelUpdate() {
+        val events = startCollectingAccessibilityEvents(rootView)
+
+        dialog.updateState(current = 1, max = MAX_LEVEL)
+
+        assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(1))
+        assertThat(events).contains(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
+    }
+
+    private fun contentDescriptionForLevel(level: Int): String {
+        return String.format(descriptionString, level, MAX_LEVEL)
+    }
+
+    private fun startCollectingAccessibilityEvents(rootView: View): MutableList<Int> {
+        val events = mutableListOf<Int>()
+        rootView.accessibilityDelegate =
+            object : View.AccessibilityDelegate() {
+                override fun sendAccessibilityEvent(host: View, eventType: Int) {
+                    super.sendAccessibilityEvent(host, eventType)
+                    events.add(eventType)
+                }
+            }
+        return events
+    }
+
+    companion object {
+        private const val MAX_LEVEL = 5
+        private const val INITIAL_LEVEL = 0
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 814a317..b16c352 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -194,6 +194,7 @@
     private @Captor ArgumentCaptor<KeyguardUpdateMonitorCallback>
             mKeyguardUpdateMonitorCallbackCaptor;
     private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
+    private FakeExecutor mUiMainExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
     private FalsingCollectorFake mFalsingCollector;
@@ -247,6 +248,7 @@
                 mConfigurationController,
                 mViewMediator,
                 mKeyguardBypassController,
+                mUiMainExecutor,
                 mUiBgExecutor,
                 mColorExtractor,
                 mDumpManager,
@@ -255,7 +257,8 @@
                 mAuthController,
                 () -> mShadeInteractor,
                 mShadeWindowLogger,
-                () -> mSelectedUserInteractor);
+                () -> mSelectedUserInteractor,
+                mUserTracker);
         mFeatureFlags = new FakeFeatureFlags();
         mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false);
         mFeatureFlags.set(Flags.REFACTOR_GETCURRENTUSER, true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index 4f6ec71..b439fcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -18,23 +18,34 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
-import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.keyguard.util.mockTopActivityClassName
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import dagger.BindsInstance
+import dagger.Component
 import dagger.Lazy
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertTrue
 import junit.framework.Assert.fail
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -49,20 +60,30 @@
             underTest
         }
 
+    private lateinit var testComponent: TestComponent
+    @Mock private lateinit var activityManagerWrapper: ActivityManagerWrapper
+
+    private var topActivityClassName = "launcher"
+
     @Before
     override fun setUp() {
         super.setUp()
+        MockitoAnnotations.initMocks(this)
 
-        underTest =
-            FromLockscreenTransitionInteractor(
-                transitionRepository = super.transitionRepository,
-                transitionInteractor = super.transitionInteractor,
-                scope = super.testScope.backgroundScope,
-                keyguardInteractor = super.keyguardInteractor,
-                flags = FakeFeatureFlags(),
-                shadeRepository = FakeShadeRepository(),
-                powerInteractor = PowerInteractorFactory.create().powerInteractor,
-            )
+        testComponent =
+            DaggerFromLockscreenTransitionInteractorTest_TestComponent.factory()
+                .create(
+                    test = this,
+                    mocks =
+                        TestMocksModule(
+                            activityManagerWrapper = activityManagerWrapper,
+                        ),
+                )
+        underTest = testComponent.underTest
+        testScope = testComponent.testScope
+        transitionRepository = testComponent.transitionRepository
+
+        activityManagerWrapper.mockTopActivityClassName(topActivityClassName)
     }
 
     @Test
@@ -189,4 +210,73 @@
                 fail("surfaceBehindModel was unexpectedly null.")
             }
         }
+
+    @Test
+    fun testSurfaceBehindModel_alpha1_whenTransitioningWithInWindowAnimation() =
+        testScope.runTest {
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                topActivityClassName
+            )
+            runCurrent()
+
+            val values by collectValues(underTest.surfaceBehindModel)
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(1f, values[values.size - 1]?.alpha)
+        }
+
+    @Test
+    fun testSurfaceBehindModel_alphaZero_whenNotTransitioningWithInWindowAnimation() =
+        testScope.runTest {
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                "not_launcher"
+            )
+            runCurrent()
+
+            val values by collectValues(underTest.surfaceBehindModel)
+            runCurrent()
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(0f, values[values.size - 1]?.alpha)
+        }
+
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+            ]
+    )
+    interface TestComponent {
+        val underTest: FromLockscreenTransitionInteractor
+        val testScope: TestScope
+        val transitionRepository: FakeKeyguardTransitionRepository
+        val surfaceBehindRepository: FakeKeyguardSurfaceBehindRepository
+        val inWindowLauncherUnlockAnimationRepository: InWindowLauncherUnlockAnimationRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
new file mode 100644
index 0000000..7fb0dd5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.TestMocksModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.mockTopActivityClassName
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import dagger.BindsInstance
+import dagger.Component
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class InWindowLauncherUnlockAnimationInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: InWindowLauncherUnlockAnimationInteractor
+
+    private lateinit var testComponent: TestComponent
+    private lateinit var testScope: TestScope
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+    @Mock private lateinit var activityManagerWrapper: ActivityManagerWrapper
+
+    private val launcherClassName = "launcher"
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        testComponent =
+            DaggerInWindowLauncherUnlockAnimationInteractorTest_TestComponent.factory()
+                .create(
+                    test = this,
+                    mocks =
+                        TestMocksModule(
+                            activityManagerWrapper = activityManagerWrapper,
+                        ),
+                )
+        underTest = testComponent.underTest
+        testScope = testComponent.testScope
+        transitionRepository = testComponent.transitionRepository
+
+        activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+    }
+
+    @Test
+    fun testTransitioningToGoneWithInWindowAnimation_trueIfTopActivityIsLauncher_andTransitioningToGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.transitioningToGoneWithInWindowAnimation)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            // Put launcher on top
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                launcherClassName
+            )
+            activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+            runCurrent()
+
+            // Should still be false since we're not transitioning to GONE.
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true, // -> GONE + launcher is behind
+                ),
+                values
+            )
+
+            activityManagerWrapper.mockTopActivityClassName("not_launcher")
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true, // Top activity should be sampled, if it changes midway it should not
+                    // matter.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true,
+                    false, // False once we're not transitioning anymore.
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testTransitioningToGoneWithInWindowAnimation_falseIfTopActivityIsLauncherPartwayThrough() =
+        testScope.runTest {
+            val values by collectValues(underTest.transitioningToGoneWithInWindowAnimation)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            // Put not launcher on top
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                launcherClassName
+            )
+            activityManagerWrapper.mockTopActivityClassName("not_launcher")
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+
+            activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.RUNNING,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testTransitioningToGoneWithInWindowAnimation_falseIfTopActivityIsLauncherWhileNotTransitioningToGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.transitioningToGoneWithInWindowAnimation)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            // Put launcher on top
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                launcherClassName
+            )
+            activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testShouldStartInWindowAnimation_trueOnceSurfaceAvailable_falseWhenTransitionEnds() =
+        testScope.runTest {
+            val values by collectValues(underTest.shouldStartInWindowAnimation)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            // Put Launcher on top and begin transitioning to GONE.
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                launcherClassName
+            )
+            activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+
+            testComponent.surfaceBehindRepository.setSurfaceRemoteAnimationTargetAvailable(true)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true, // The surface is now available, so we should start the animation.
+                ),
+                values
+            )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                    true,
+                    false,
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testShouldStartInWindowAnimation_neverTrueIfSurfaceNotAvailable() =
+        testScope.runTest {
+            val values by collectValues(underTest.shouldStartInWindowAnimation)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            // Put Launcher on top and begin transitioning to GONE.
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                launcherClassName
+            )
+            activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.FINISHED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+        }
+
+    @Test
+    fun testShouldStartInWindowAnimation_falseIfSurfaceAvailable_afterTransitionInterrupted() =
+        testScope.runTest {
+            val values by collectValues(underTest.shouldStartInWindowAnimation)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false, // False by default.
+                ),
+                values
+            )
+
+            // Put Launcher on top and begin transitioning to GONE.
+            testComponent.inWindowLauncherUnlockAnimationRepository.setLauncherActivityClass(
+                launcherClassName
+            )
+            activityManagerWrapper.mockTopActivityClassName(launcherClassName)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.AOD,
+                )
+            )
+            testComponent.surfaceBehindRepository.setSurfaceRemoteAnimationTargetAvailable(true)
+            runCurrent()
+
+            assertEquals(
+                listOf(
+                    false,
+                ),
+                values
+            )
+        }
+
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+            ]
+    )
+    interface TestComponent {
+        val underTest: InWindowLauncherUnlockAnimationInteractor
+        val testScope: TestScope
+        val transitionRepository: FakeKeyguardTransitionRepository
+        val surfaceBehindRepository: FakeKeyguardSurfaceBehindRepository
+        val inWindowLauncherUnlockAnimationRepository: InWindowLauncherUnlockAnimationRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
index 8db19ae..339fd22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
@@ -26,7 +26,7 @@
 
 open class KeyguardTransitionInteractorTestCase : SysuiTestCase() {
     val testDispatcher = StandardTestDispatcher()
-    val testScope = TestScope(testDispatcher)
+    var testScope = TestScope(testDispatcher)
 
     lateinit var keyguardRepository: FakeKeyguardRepository
     lateinit var transitionRepository: FakeKeyguardTransitionRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 275ac80..c292102 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -27,7 +27,9 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
@@ -44,6 +46,7 @@
 import com.android.systemui.shade.domain.model.ShadeModel
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
@@ -147,6 +150,15 @@
                     flags = featureFlags,
                     shadeRepository = shadeRepository,
                     powerInteractor = powerInteractor,
+                    inWindowLauncherUnlockAnimationInteractor = {
+                        InWindowLauncherUnlockAnimationInteractor(
+                            InWindowLauncherUnlockAnimationRepository(),
+                            testScope,
+                            transitionInteractor,
+                            { FakeKeyguardSurfaceBehindRepository() },
+                            mock(),
+                        )
+                    },
                 )
                 .apply { start() }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
new file mode 100644
index 0000000..570dfb3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
+import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
+import com.android.systemui.util.mockito.any
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class InWindowLauncherUnlockAnimationManagerTest : SysuiTestCase() {
+    private lateinit var underTest: InWindowLauncherUnlockAnimationManager
+
+    private lateinit var testComponent: TestComponent
+    private lateinit var testScope: TestScope
+
+    @Mock private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        testComponent =
+            DaggerInWindowLauncherUnlockAnimationManagerTest_TestComponent.factory()
+                .create(
+                    test = this,
+                )
+        underTest = testComponent.underTest
+        testScope = testComponent.testScope
+
+        underTest.setLauncherUnlockController("launcherClass", launcherUnlockAnimationController)
+    }
+
+    @Test
+    fun testPrepareForUnlock_calledOnlyOnce() =
+        testScope.runTest {
+            underTest.prepareForUnlock()
+            underTest.prepareForUnlock()
+
+            verify(launcherUnlockAnimationController)
+                .prepareForUnlock(anyBoolean(), any(), anyInt())
+        }
+
+    @Test
+    fun testPlayUnlockAnimation_onlyCalledIfPrepared() =
+        testScope.runTest {
+            underTest.playUnlockAnimation(true, 200, 0)
+            verify(launcherUnlockAnimationController, never())
+                .playUnlockAnimation(any(), any(), any())
+        }
+
+    @Test
+    fun testForceUnlocked_ifPreparedButNeverStarted() =
+        testScope.runTest {
+            underTest.prepareForUnlock()
+            underTest.ensureUnlockedOrAnimatingUnlocked()
+
+            verify(launcherUnlockAnimationController).setUnlockAmount(1f, true)
+        }
+
+    @Test
+    fun testForceUnlocked_ifManualUnlockAmountLessThan1() =
+        testScope.runTest {
+            underTest.prepareForUnlock()
+            underTest.setUnlockAmount(0.5f, false)
+            underTest.ensureUnlockedOrAnimatingUnlocked()
+
+            verify(launcherUnlockAnimationController).prepareForUnlock(any(), any(), any())
+            verify(launcherUnlockAnimationController).setUnlockAmount(0.5f, false)
+            verify(launcherUnlockAnimationController).setUnlockAmount(1f, true)
+            verifyNoMoreInteractions(launcherUnlockAnimationController)
+        }
+
+    @Test
+    fun testDoesNotForceUnlocked_ifNeverPrepared() =
+        testScope.runTest {
+            underTest.ensureUnlockedOrAnimatingUnlocked()
+
+            verifyNoMoreInteractions(launcherUnlockAnimationController)
+        }
+
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+            ]
+    )
+    interface TestComponent {
+        val underTest: InWindowLauncherUnlockAnimationManager
+        val testScope: TestScope
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+            ): TestComponent
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
index c7f7c3c..71313c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
@@ -26,6 +26,7 @@
 import com.android.keyguard.LockIconViewController
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
@@ -59,9 +60,11 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+
         featureFlags =
             FakeFeatureFlagsClassic().apply {
-                set(Flags.MIGRATE_LOCK_ICON, false)
                 set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
                 set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
             }
@@ -81,7 +84,7 @@
 
     @Test
     fun addViewsConditionally_migrateFlagOn() {
-        featureFlags.set(Flags.MIGRATE_LOCK_ICON, true)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isGreaterThan(0)
@@ -89,7 +92,7 @@
 
     @Test
     fun addViewsConditionally_migrateAndRefactorFlagsOn() {
-        featureFlags.set(Flags.MIGRATE_LOCK_ICON, true)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
@@ -98,7 +101,7 @@
 
     @Test
     fun addViewsConditionally_migrateFlagOff() {
-        featureFlags.set(Flags.MIGRATE_LOCK_ICON, false)
+        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
index 8b8c59b..8dd33d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
@@ -23,12 +23,10 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -43,7 +41,6 @@
     @Mock private lateinit var keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel
     @Mock private lateinit var keyguardRootViewModel: KeyguardRootViewModel
     @Mock private lateinit var indicationController: KeyguardIndicationController
-    @Mock private lateinit var featureFlags: FeatureFlags
 
     private lateinit var underTest: DefaultIndicationAreaSection
 
@@ -56,13 +53,12 @@
                 keyguardIndicationAreaViewModel,
                 keyguardRootViewModel,
                 indicationController,
-                featureFlags,
             )
     }
 
     @Test
     fun addViewsConditionally() {
-        whenever(featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)).thenReturn(true)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isGreaterThan(0)
@@ -70,7 +66,7 @@
 
     @Test
     fun addViewsConditionally_migrateFlagOff() {
-        whenever(featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)).thenReturn(false)
+        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isEqualTo(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
index 34d93fc..88a4aa5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
@@ -29,7 +28,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -40,14 +38,12 @@
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
 
     @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
     @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
-    @Mock private lateinit var featureFlags: FeatureFlags
 
     private lateinit var underTest: KeyguardIndicationAreaViewModel
     private lateinit var repository: FakeKeyguardRepository
@@ -87,7 +83,6 @@
                 keyguardBottomAreaViewModel = bottomAreaViewModel,
                 burnInHelperWrapper = burnInHelperWrapper,
                 shortcutsCombinedViewModel = shortcutsCombinedViewModel,
-                featureFlags = featureFlags,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 1c6cc87..25d1419 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dock.DockManagerFake
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
@@ -123,9 +124,11 @@
             FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
         dockManager = DockManagerFake()
         biometricSettingsRepository = FakeBiometricSettingsRepository()
+
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+
         val featureFlags =
             FakeFeatureFlags().apply {
-                set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true)
                 set(Flags.FACE_AUTH_REFACTOR, true)
                 set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
                 set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 985b6fd..259c74f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -106,9 +107,10 @@
         testScope = TestScope(testDispatcher)
         MockitoAnnotations.initMocks(this)
 
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+
         val featureFlags =
             FakeFeatureFlagsClassic().apply {
-                set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true)
                 set(Flags.FACE_AUTH_REFACTOR, true)
             }
 
@@ -351,7 +353,6 @@
                 featureFlags =
                     FakeFeatureFlagsClassicModule {
                         setDefault(Flags.NEW_AOD_TRANSITION)
-                        set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true)
                         set(Flags.FACE_AUTH_REFACTOR, true)
                     },
                 mocks =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/ActivityManagerWrapperMock.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/ActivityManagerWrapperMock.kt
new file mode 100644
index 0000000..2cb7e65
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/ActivityManagerWrapperMock.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.util
+
+import android.app.ActivityManager
+import android.content.ComponentName
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+/**
+ * Configures an ActivityManagerWrapper mock to return the given class name whenever we ask for the
+ * running task's top activity class name.
+ */
+fun ActivityManagerWrapper.mockTopActivityClassName(name: String) {
+    val topActivityMock = mock<ComponentName>().apply { whenever(className).thenReturn(name) }
+
+    whenever(runningTask)
+        .thenReturn(ActivityManager.RunningTaskInfo().apply { topActivity = topActivityMock })
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
index c835146..8a531fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
@@ -111,7 +111,6 @@
                         TaskStackChangeListeners.getTestInstance(),
                         Optional.of(mock(Pip.class)),
                         Optional.of(mock(BackAnimation.class)),
-                        mock(FeatureFlags.class),
                         mock(SecureSettings.class),
                         mDisplayTracker));
         initializeNavigationBars();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 48a36cb..ddceed6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -27,6 +27,7 @@
 import static android.view.WindowInsets.Type.ime;
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
+import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
@@ -42,6 +43,7 @@
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -86,6 +88,7 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
 import com.android.systemui.navigationbar.buttons.DeadZone;
+import com.android.systemui.navigationbar.buttons.KeyButtonView;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
@@ -120,6 +123,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -143,6 +147,8 @@
     @Mock
     ButtonDispatcher mHomeButton;
     @Mock
+    KeyButtonView mHomeButtonView;
+    @Mock
     ButtonDispatcher mRecentsButton;
     @Mock
     ButtonDispatcher mAccessibilityButton;
@@ -294,11 +300,38 @@
 
     @Test
     public void testHomeLongPress() {
+        when(mAssistManager.shouldOverrideAssist(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS))
+                .thenReturn(false);
+
         mNavigationBar.init();
         mNavigationBar.onViewAttached();
-        mNavigationBar.onHomeLongClick(mNavigationBar.getView());
+        mNavigationBar.onHomeLongClick(mHomeButtonView);
 
         verify(mUiEventLogger, times(1)).log(NAVBAR_ASSIST_LONGPRESS);
+        verify(mAssistManager).startAssist(any());
+    }
+
+    @Test
+    public void testHomeLongPressOverride() {
+        when(mAssistManager.shouldOverrideAssist(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS))
+                .thenReturn(true);
+
+        mNavigationBar.init();
+        mNavigationBar.onViewAttached();
+        mNavigationBar.onHomeLongClick(mHomeButtonView);
+
+        verify(mUiEventLogger, times(1)).log(NAVBAR_ASSIST_LONGPRESS);
+
+        ArgumentCaptor<Runnable> onRippleInvisibleRunnableCaptor = ArgumentCaptor.forClass(
+                Runnable.class);
+        // startAssist is not called initially
+        verify(mAssistManager, never()).startAssist(any());
+        // but a Runnable is added for when the ripple is invisible
+        verify(mHomeButtonView).setOnRippleInvisibleRunnable(
+                onRippleInvisibleRunnableCaptor.capture());
+        // and when that runs, startAssist is called
+        onRippleInvisibleRunnableCaptor.getValue().run();
+        verify(mAssistManager).startAssist(any());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java
index 078a917..a1010a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java
@@ -50,6 +50,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.assist.AssistManager;
 import com.android.systemui.recents.OverviewProxyService;
 
 import org.junit.Before;
@@ -76,6 +77,7 @@
         MockitoAnnotations.initMocks(this);
         mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class);
         mDependency.injectMockDependency(OverviewProxyService.class);
+        mDependency.injectMockDependency(AssistManager.class);
         mUiEventLogger = mDependency.injectMockDependency(UiEventLogger.class);
 
         TestableLooper.get(this).runWithLooper(() -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt
index 95ee3b7..bd1c310 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt
@@ -41,7 +41,7 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = QSTileIntentUserInputHandler(activityStarted)
+        underTest = QSTileIntentUserInputHandlerImpl(activityStarted)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
index 31d02ed..8f27e4e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt
@@ -57,7 +57,7 @@
         whenever(logBufferFactory.create(any(), any(), any())).thenReturn(logBuffer)
         underTest =
             QSTileLogger(
-                mapOf(TileSpec.create("chatty_tile") to chattyLogBuffer),
+                mapOf("chatty_tile" to chattyLogBuffer),
                 logBufferFactory,
                 statusBarController
             )
@@ -117,7 +117,7 @@
         underTest.logUserActionPipeline(
             TileSpec.create("test_spec"),
             QSTileUserAction.Click(null),
-            QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {},
+            QSTileState.build({ Icon.Resource(0, ContentDescription.Resource(0)) }, "") {},
             "test_data",
         )
 
@@ -143,7 +143,7 @@
     fun testLogStateUpdate() {
         underTest.logStateUpdate(
             TileSpec.create("test_spec"),
-            QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {},
+            QSTileState.build({ Icon.Resource(0, ContentDescription.Resource(0)) }, "") {},
             "test_data",
         )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
index 9bf4a75..d3b7daa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
@@ -97,7 +97,10 @@
             {
                 object : QSTileDataToStateMapper<Any> {
                     override fun map(config: QSTileConfig, data: Any): QSTileState =
-                        QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}
+                        QSTileState.build(
+                            { Icon.Resource(0, ContentDescription.Resource(0)) },
+                            ""
+                        ) {}
                 }
             },
             fakeDisabledByPolicyInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 6d358db..70a48f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -30,8 +30,10 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
 import com.android.systemui.model.SysUiState
 import com.android.systemui.navigationbar.NavigationBarController
 import com.android.systemui.navigationbar.NavigationModeController
@@ -100,6 +102,9 @@
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var sysuiUnlockAnimationController: KeyguardUnlockAnimationController
+    @Mock
+    private lateinit var inWindowLauncherUnlockAnimationManager:
+        InWindowLauncherUnlockAnimationManager
     @Mock private lateinit var assistUtils: AssistUtils
     @Mock
     private lateinit var unfoldTransitionProgressForwarder:
@@ -126,6 +131,7 @@
         whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt()))
             .thenReturn(mock(ResolveInfo::class.java))
 
+        featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false)
         subject =
             OverviewProxyService(
                 context,
@@ -144,6 +150,7 @@
                 uiEventLogger,
                 displayTracker,
                 sysuiUnlockAnimationController,
+                inWindowLauncherUnlockAnimationManager,
                 assistUtils,
                 featureFlags,
                 FakeSceneContainerFlags(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
index 32a38bd..4c8b562 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
@@ -67,6 +67,7 @@
 
         listOf(
                 AconfigFlags.FLAG_SCENE_CONTAINER,
+                AconfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
             )
             .forEach { flagToken ->
                 setFlagsRule.enableFlags(flagToken)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 55a44ae..e6cd17f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -59,9 +59,12 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -74,8 +77,10 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
 import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
 import com.android.systemui.scene.shared.logger.SceneLogger;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
@@ -132,8 +137,10 @@
     @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
+    @Mock private UserTracker mUserTracker;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
+    private final Executor mMainExecutor = MoreExecutors.directExecutor();
     private final Executor mBackgroundExecutor = MoreExecutors.directExecutor();
     private SceneTestUtils mUtils = new SceneTestUtils(this);
     private TestScope mTestScope = mUtils.getTestScope();
@@ -207,7 +214,16 @@
                 keyguardInteractor,
                 featureFlags,
                 shadeRepository,
-                powerInteractor);
+                powerInteractor,
+                () ->
+                        new InWindowLauncherUnlockAnimationInteractor(
+                                new InWindowLauncherUnlockAnimationRepository(),
+                                mTestScope.getBackgroundScope(),
+                                keyguardTransitionInteractor,
+                                () -> new FakeKeyguardSurfaceBehindRepository(),
+                                mock(ActivityManagerWrapper.class)
+                        )
+                );
 
         mFromPrimaryBouncerTransitionInteractor = new FromPrimaryBouncerTransitionInteractor(
                 keyguardTransitionRepository,
@@ -248,6 +264,7 @@
                 mConfigurationController,
                 mKeyguardViewMediator,
                 mKeyguardBypassController,
+                mMainExecutor,
                 mBackgroundExecutor,
                 mColorExtractor,
                 mDumpManager,
@@ -256,7 +273,8 @@
                 mAuthController,
                 () -> mShadeInteractor,
                 mShadeWindowLogger,
-                () -> mSelectedUserInteractor) {
+                () -> mSelectedUserInteractor,
+                mUserTracker) {
                     @Override
                     protected boolean isDebuggable() {
                         return false;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index d4b35a9..6d04887 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -45,9 +45,12 @@
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -68,6 +71,7 @@
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -241,7 +245,16 @@
                 keyguardInteractor,
                 featureFlags,
                 mShadeRepository,
-                powerInteractor);
+                powerInteractor,
+                () ->
+                        new InWindowLauncherUnlockAnimationInteractor(
+                                new InWindowLauncherUnlockAnimationRepository(),
+                                mTestScope.getBackgroundScope(),
+                                keyguardTransitionInteractor,
+                                () -> new FakeKeyguardSurfaceBehindRepository(),
+                                mock(ActivityManagerWrapper.class)
+                        )
+                );
 
         mFromPrimaryBouncerTransitionInteractor = new FromPrimaryBouncerTransitionInteractor(
                 keyguardTransitionRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index d6dfc5e..4b79a49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -29,9 +29,12 @@
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -131,7 +134,16 @@
             keyguardInteractor,
             featureFlags,
             shadeRepository,
-            powerInteractor)
+            powerInteractor,
+            {
+                InWindowLauncherUnlockAnimationInteractor(
+                    InWindowLauncherUnlockAnimationRepository(),
+                    testScope,
+                    keyguardTransitionInteractor,
+                    { FakeKeyguardSurfaceBehindRepository() },
+                    mock(),
+                )
+            })
         fromPrimaryBouncerTransitionInteractor = FromPrimaryBouncerTransitionInteractor(
             keyguardTransitionRepository,
             keyguardTransitionInteractor,
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 2e191b1..a580600 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -102,9 +102,12 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
+import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.model.SysUiState;
@@ -126,6 +129,7 @@
 import com.android.systemui.shade.ShadeWindowLogger;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationEntryHelper;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -330,6 +334,8 @@
     @Mock
     private SelectedUserInteractor mSelectedUserInteractor;
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private NotifPipelineFlags mNotifPipelineFlags;
     @Mock
     private Icon mAppBubbleIcon;
@@ -429,7 +435,16 @@
                 keyguardInteractor,
                 featureFlags,
                 shadeRepository,
-                powerInteractor);
+                powerInteractor,
+                () ->
+                        new InWindowLauncherUnlockAnimationInteractor(
+                                new InWindowLauncherUnlockAnimationRepository(),
+                                mTestScope.getBackgroundScope(),
+                                keyguardTransitionInteractor,
+                                () -> new FakeKeyguardSurfaceBehindRepository(),
+                                mock(ActivityManagerWrapper.class)
+                        )
+                );
 
         mFromPrimaryBouncerTransitionInteractor = new FromPrimaryBouncerTransitionInteractor(
                 keyguardTransitionRepository,
@@ -475,6 +490,7 @@
                 mKeyguardViewMediator,
                 mKeyguardBypassController,
                 syncExecutor,
+                syncExecutor,
                 mColorExtractor,
                 mDumpManager,
                 mKeyguardStateController,
@@ -482,7 +498,8 @@
                 mAuthController,
                 () -> mShadeInteractor,
                 mShadeWindowLogger,
-                () -> mSelectedUserInteractor
+                () -> mSelectedUserInteractor,
+                mUserTracker
         );
         mNotificationShadeWindowController.fetchWindowRootView();
         mNotificationShadeWindowController.attach();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 08adda3..8a2ff50 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -1,6 +1,5 @@
 package com.android.systemui.communal.data.repository
 
-import com.android.systemui.communal.data.model.CommunalWidgetMetadata
 import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import kotlinx.coroutines.flow.Flow
@@ -10,7 +9,6 @@
 class FakeCommunalWidgetRepository : CommunalWidgetRepository {
     private val _stopwatchAppWidgetInfo = MutableStateFlow<CommunalAppWidgetInfo?>(null)
     override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = _stopwatchAppWidgetInfo
-    override var communalWidgetAllowlist: List<CommunalWidgetMetadata> = emptyList()
 
     private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList())
     override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
index abf72af..6838e76 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
@@ -17,6 +17,7 @@
 
 import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule
 import dagger.Module
 
@@ -26,6 +27,7 @@
             FakeCommandQueueModule::class,
             FakeKeyguardRepositoryModule::class,
             FakeKeyguardTransitionRepositoryModule::class,
+            FakeKeyguardSurfaceBehindRepositoryModule::class,
         ]
 )
 object FakeKeyguardDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt
index 823f29a..70de05f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardSurfaceBehindRepository.kt
@@ -16,14 +16,31 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakeKeyguardSurfaceBehindRepository : KeyguardSurfaceBehindRepository {
+@SysUISingleton
+class FakeKeyguardSurfaceBehindRepository @Inject constructor() : KeyguardSurfaceBehindRepository {
     private val _isAnimatingSurface = MutableStateFlow(false)
     override val isAnimatingSurface = _isAnimatingSurface.asStateFlow()
 
+    private val _isSurfaceAvailable = MutableStateFlow(false)
+    override val isSurfaceRemoteAnimationTargetAvailable = _isSurfaceAvailable.asStateFlow()
+
     override fun setAnimatingSurface(animating: Boolean) {
         _isAnimatingSurface.value = animating
     }
+
+    override fun setSurfaceRemoteAnimationTargetAvailable(available: Boolean) {
+        _isSurfaceAvailable.value = available
+    }
+}
+
+@Module
+interface FakeKeyguardSurfaceBehindRepositoryModule {
+    @Binds fun bindFake(fake: FakeKeyguardSurfaceBehindRepository): KeyguardSurfaceBehindRepository
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
new file mode 100644
index 0000000..1185f2e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.base.actions
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.view.View
+
+/**
+ * Fake implementation of [QSTileIntentUserInputHandler] interface. Consider using this alongside
+ * [QSTileIntentUserInputHandlerSubject].
+ */
+class FakeQSTileIntentUserInputHandler : QSTileIntentUserInputHandler {
+
+    val handledInputs: List<Input>
+        get() = mutableInputs
+
+    private val mutableInputs = mutableListOf<Input>()
+
+    override fun handle(view: View?, intent: Intent) {
+        mutableInputs.add(Input.Intent(view, intent))
+    }
+
+    override fun handle(view: View?, pendingIntent: PendingIntent) {
+        mutableInputs.add(Input.PendingIntent(view, pendingIntent))
+    }
+
+    sealed interface Input {
+        data class Intent(val view: View?, val intent: android.content.Intent) : Input
+        data class PendingIntent(val view: View?, val pendingIntent: android.app.PendingIntent) :
+            Input
+    }
+}
+
+val FakeQSTileIntentUserInputHandler.intentInputs
+    get() = handledInputs.mapNotNull { it as? FakeQSTileIntentUserInputHandler.Input.Intent }
+val FakeQSTileIntentUserInputHandler.pendingIntentInputs
+    get() = handledInputs.mapNotNull { it as? FakeQSTileIntentUserInputHandler.Input.PendingIntent }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt
new file mode 100644
index 0000000..e09f280
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.base.actions
+
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth
+
+/** [Truth] [Subject] to assert inputs handled by [FakeQSTileIntentUserInputHandler] */
+class QSTileIntentUserInputHandlerSubject
+private constructor(
+    failureMetadata: FailureMetadata,
+    private val subject: FakeQSTileIntentUserInputHandler
+) : Subject(failureMetadata, subject) {
+
+    fun handledOneIntentInput(
+        intentAssertions: (FakeQSTileIntentUserInputHandler.Input.Intent) -> Unit = {},
+    ) {
+        // check that there are no other inputs
+        check("handledInputs").that(subject.handledInputs).hasSize(1)
+        // check there is an intent input
+        check("intentInputs").that(subject.intentInputs).hasSize(1)
+
+        intentAssertions(subject.intentInputs.first())
+    }
+
+    fun handledOnePendingIntentInput(
+        intentAssertions: (FakeQSTileIntentUserInputHandler.Input.PendingIntent) -> Unit = {},
+    ) {
+        // check that there are no other inputs
+        check("handledInputs").that(subject.handledInputs).hasSize(1)
+        // check there is a pending intent input
+        check("intentInputs").that(subject.pendingIntentInputs).hasSize(1)
+
+        intentAssertions(subject.pendingIntentInputs.first())
+    }
+
+    fun handledNoInputs() {
+        check("handledInputs").that(subject.handledInputs).isEmpty()
+    }
+
+    companion object {
+
+        /**
+         * [Truth.assertThat]-like factory to initialize the assertion. Example:
+         * ```
+         *  assertThat(inputHandler).handledOneIntentInput {
+         *      assertThat(it.intent.action).isEqualTo("action.Test")
+         *  }
+         * ```
+         */
+        fun assertThat(
+            handler: FakeQSTileIntentUserInputHandler
+        ): QSTileIntentUserInputHandlerSubject =
+            Truth.assertAbout {
+                    failureMetadata: FailureMetadata,
+                    subject: FakeQSTileIntentUserInputHandler ->
+                    QSTileIntentUserInputHandlerSubject(failureMetadata, subject)
+                }
+                .that(handler)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
new file mode 100644
index 0000000..832b07a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.base.interactor
+
+import android.os.UserHandle
+import android.view.View
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+
+object QSTileInputTestKtx {
+
+    fun <T> click(
+        data: T,
+        user: UserHandle = UserHandle.CURRENT,
+        view: View? = null,
+    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.Click(view), data)
+
+    fun <T> longClick(
+        data: T,
+        user: UserHandle = UserHandle.CURRENT,
+        view: View? = null,
+    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.LongClick(view), data)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 10110e2..f97d6b3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.scene
 
+import android.content.Context
 import android.content.pm.UserInfo
 import android.graphics.Bitmap
 import android.graphics.drawable.BitmapDrawable
@@ -81,8 +82,10 @@
  */
 @OptIn(ExperimentalCoroutinesApi::class)
 class SceneTestUtils(
-    test: SysuiTestCase,
+    private val context: Context,
 ) {
+    constructor(test: SysuiTestCase) : this(context = test.context)
+
     val kosmos = Kosmos()
     val testDispatcher = kosmos.testDispatcher
     val testScope = kosmos.testScope
@@ -115,8 +118,6 @@
         }
     }
 
-    private val context = test.context
-
     private val falsingCollectorFake: FalsingCollector by lazy { FalsingCollectorFake() }
     private var falsingInteractor: FalsingInteractor? = null
 
diff --git a/proto/src/criticalevents/critical_event_log.proto b/proto/src/criticalevents/critical_event_log.proto
index 25814ec..9cda267 100644
--- a/proto/src/criticalevents/critical_event_log.proto
+++ b/proto/src/criticalevents/critical_event_log.proto
@@ -59,8 +59,11 @@
     AppNotResponding anr = 4;
     JavaCrash java_crash = 5;
     NativeCrash native_crash = 6;
+    SystemServerStarted system_server_started = 7;
   }
 
+  message SystemServerStarted {}
+
   message Watchdog {
     // The watchdog subject.
     // Required.
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java
index 46b4628..9c6dfc6 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java
@@ -31,7 +31,7 @@
  * This class matches multi-finger multi-tap gestures. The number of fingers and the number of taps
  * for each instance is specified in the constructor.
  */
-class MultiFingerMultiTap extends GestureMatcher {
+public class MultiFingerMultiTap extends GestureMatcher {
 
     // The target number of taps.
     final int mTargetTapCount;
@@ -56,7 +56,7 @@
      * @throws IllegalArgumentException if <code>fingers<code/> is less than 2
      *                                  or <code>taps<code/> is not positive.
      */
-    MultiFingerMultiTap(
+    public MultiFingerMultiTap(
             Context context,
             int fingers,
             int taps,
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java
index 9c54100..f586036f 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java
@@ -23,9 +23,9 @@
  * This class matches gestures of the form multi-finger multi-tap and hold. The number of fingers
  * and taps for each instance is specified in the constructor.
  */
-class MultiFingerMultiTapAndHold extends MultiFingerMultiTap {
+public class MultiFingerMultiTapAndHold extends MultiFingerMultiTap {
 
-    MultiFingerMultiTapAndHold(
+    public MultiFingerMultiTapAndHold(
             Context context,
             int fingers,
             int taps,
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
index 5953d0d..36e75118 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
@@ -41,6 +41,8 @@
 import com.android.server.accessibility.EventStreamTransformation;
 import com.android.server.accessibility.Flags;
 import com.android.server.accessibility.gestures.GestureMatcher;
+import com.android.server.accessibility.gestures.MultiFingerMultiTap;
+import com.android.server.accessibility.gestures.MultiFingerMultiTapAndHold;
 import com.android.server.accessibility.gestures.MultiTap;
 import com.android.server.accessibility.gestures.MultiTapAndHold;
 
@@ -476,6 +478,15 @@
                         null));
                 mGestureMatchers.add(new TwoFingersDownOrSwipe(context));
 
+                if (mDetectTwoFingerTripleTap) {
+                    mGestureMatchers.add(new MultiFingerMultiTap(context, /* fingers= */ 2,
+                            /* taps= */ 3, MagnificationGestureMatcher.GESTURE_TRIPLE_TAP,
+                            null));
+                    mGestureMatchers.add(new MultiFingerMultiTapAndHold(context, /* fingers= */ 2,
+                            /* taps= */ 3, MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD,
+                            null));
+                }
+
                 mGesturesObserver = new MagnificationGesturesObserver(this,
                         mGestureMatchers.toArray(new GestureMatcher[mGestureMatchers.size()]));
             } else {
@@ -512,7 +523,9 @@
         @Override
         public boolean shouldStopDetection(MotionEvent motionEvent) {
             return !mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId)
-                    && !mDetectSingleFingerTripleTap;
+                    && !mDetectSingleFingerTripleTap
+                    && !(mDetectTwoFingerTripleTap
+                    && Flags.enableMagnificationMultipleFingerMultipleTapGesture());
         }
 
         @Override
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index c4cb816..256f2b3 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -180,14 +180,7 @@
 # Snapshot rebuild instance
 3131 pm_snapshot_rebuild (build_time|1|3),(lifetime|1|3)
 # Caller information to clear application data
-1003160 pm_clear_app_data_caller (pid|1),(uid|1),(package|3)
-# ---------------------------
-# Installer.java
-# ---------------------------
-# Caller Information to clear application data
-1003200 installer_clear_app_data_caller (pid|1),(uid|1),(package|3),(flags|1)
-# Call stack to clear application data
-1003201 installer_clear_app_data_call_stack (method|3),(class|3),(file|3),(line|1)
+3132 pm_clear_app_data_caller (pid|1),(uid|1),(package|3)
 
 # ---------------------------
 # InputMethodManagerService.java
@@ -227,6 +220,14 @@
 35000 auto_brightness_adj (old_lux|5),(old_brightness|5),(new_lux|5),(new_brightness|5)
 
 # ---------------------------
+# Installer.java
+# ---------------------------
+# Caller Information to clear application data
+39000 installer_clear_app_data_caller (pid|1),(uid|1),(package|3),(flags|1)
+# Call stack to clear application data
+39001 installer_clear_app_data_call_stack (method|3),(class|3),(file|3),(line|1)
+
+# ---------------------------
 # ConnectivityService.java
 # ---------------------------
 # Connectivity state changed
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1566113..514be15 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -79,6 +79,7 @@
 import static android.os.PowerExemptionManager.REASON_BACKGROUND_ACTIVITY_PERMISSION;
 import static android.os.PowerExemptionManager.REASON_BOOT_COMPLETED;
 import static android.os.PowerExemptionManager.REASON_COMPANION_DEVICE_MANAGER;
+import static android.os.PowerExemptionManager.REASON_DENIED;
 import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_ACTIVITY_PERMISSION;
 import static android.os.PowerExemptionManager.REASON_LOCKED_BOOT_COMPLETED;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_BTOP;
@@ -7715,14 +7716,100 @@
                 "getUidProcessState", callingPackage); // Ignore return value
 
         synchronized (mProcLock) {
-            if (mPendingStartActivityUids.isPendingTopUid(uid)) {
-                return PROCESS_STATE_TOP;
-            }
-            return mProcessList.getUidProcStateLOSP(uid);
+            return getUidProcessStateInnerLOSP(uid);
         }
     }
 
     @Override
+    public int getBindingUidProcessState(int targetUid, String callingPackage) {
+        if (!hasUsageStatsPermission(callingPackage)) {
+            enforceCallingPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE,
+                    "getBindingUidProcessState");
+        }
+        // We don't need to do a cross-user check here (unlike getUidProcessState),
+        // because we only allow to see UIDs that are actively communicating with the caller.
+
+        final int callingUid = Binder.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (this) {
+                final boolean allowed = (callingUid == targetUid)
+                        || hasServiceBindingOrProviderUseLocked(callingUid, targetUid);
+                if (!allowed) {
+                    return PROCESS_STATE_NONEXISTENT;
+                }
+                return getUidProcessStateInnerLOSP(targetUid);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @GuardedBy(anyOf = {"this", "mProcLock"})
+    private int getUidProcessStateInnerLOSP(int uid) {
+        if (mPendingStartActivityUids.isPendingTopUid(uid)) {
+            return PROCESS_STATE_TOP;
+        }
+        return mProcessList.getUidProcStateLOSP(uid);
+    }
+
+    /**
+     * Ensure that {@code clientUid} has a bound service client to {@code callingUid}
+     */
+    @GuardedBy("this")
+    private boolean hasServiceBindingOrProviderUseLocked(int callingUid, int clientUid) {
+        // See if there's a service binding
+        final Boolean hasBinding = mProcessList.searchEachLruProcessesLOSP(
+                false, pr -> {
+                    if (pr.uid == callingUid) {
+                        final ProcessServiceRecord psr = pr.mServices;
+                        final int serviceCount = psr.mServices.size();
+                        for (int svc = 0; svc < serviceCount; svc++) {
+                            final ArrayMap<IBinder, ArrayList<ConnectionRecord>> conns =
+                                    psr.mServices.valueAt(svc).getConnections();
+                            final int size = conns.size();
+                            for (int conni = 0; conni < size; conni++) {
+                                final ArrayList<ConnectionRecord> crs = conns.valueAt(conni);
+                                for (int con = 0; con < crs.size(); con++) {
+                                    final ConnectionRecord cr = crs.get(con);
+                                    final ProcessRecord clientPr = cr.binding.client;
+
+                                    if (clientPr.uid == clientUid) {
+                                        return Boolean.TRUE;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    return null;
+                });
+        if (Boolean.TRUE.equals(hasBinding)) {
+            return true;
+        }
+
+        final Boolean hasProviderClient = mProcessList.searchEachLruProcessesLOSP(
+                false, pr -> {
+                    if (pr.uid == callingUid) {
+                        final ProcessProviderRecord ppr = pr.mProviders;
+                        for (int provi = ppr.numberOfProviders() - 1; provi >= 0; provi--) {
+                            ContentProviderRecord cpr = ppr.getProviderAt(provi);
+
+                            for (int i = cpr.connections.size() - 1; i >= 0; i--) {
+                                ContentProviderConnection conn = cpr.connections.get(i);
+                                ProcessRecord client = conn.client;
+                                if (client.uid == clientUid) {
+                                    return Boolean.TRUE;
+                                }
+                            }
+                        }
+                    }
+                    return null;
+                });
+
+        return Boolean.TRUE.equals(hasProviderClient);
+    }
+
+    @Override
     public @ProcessCapability int getUidProcessCapabilities(int uid, String callingPackage) {
         if (!hasUsageStatsPermission(callingPackage)) {
             enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS,
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 68af626..a0a7b2b 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -2583,7 +2583,7 @@
 
         // Do nothing if the binder error callback is not enabled.
         // That means the frozen apps in a wrong state will be killed when they are unfrozen later.
-        if (!mFreezerBinderCallbackEnabled) {
+        if (!mUseFreezer || !mFreezerBinderCallbackEnabled) {
             return;
         }
 
diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags
index 931914f..2aed847 100644
--- a/services/core/java/com/android/server/am/EventLogTags.logtags
+++ b/services/core/java/com/android/server/am/EventLogTags.logtags
@@ -131,4 +131,4 @@
 30110 am_intent_sender_redirect_user (userId|1|5)
 
 # Caller information to clear application data
-1030002 am_clear_app_data_caller (pid|1),(uid|1),(package|3)
+30120 am_clear_app_data_caller (pid|1),(uid|1),(package|3)
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 87633e9..47a99fe 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -2030,6 +2030,9 @@
             mTargetUserId = targetUserId;
             userSwitchUiEnabled = mUserSwitchUiEnabled;
         }
+        if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) {
+            mInjector.setHasTopUi(true);
+        }
         if (userSwitchUiEnabled) {
             UserInfo currentUserInfo = getUserInfo(currentUserId);
             Pair<UserInfo, UserInfo> userNames = new Pair<>(currentUserInfo, targetUserInfo);
@@ -2098,6 +2101,9 @@
     }
 
     private void endUserSwitch() {
+        if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) {
+            mInjector.setHasTopUi(false);
+        }
         final int nextUserId;
         synchronized (mLock) {
             nextUserId = ObjectUtils.getOrElse(mPendingTargetUserIds.poll(), UserHandle.USER_NULL);
@@ -3781,6 +3787,15 @@
             getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId);
         }
 
+        void setHasTopUi(boolean hasTopUi) {
+            try {
+                Slogf.i(TAG, "Setting hasTopUi to " + hasTopUi);
+                mService.setHasTopUi(hasTopUi);
+            } catch (RemoteException e) {
+                Slogf.e(TAG, "Failed to allow using all CPU cores", e);
+            }
+        }
+
         void onSystemUserVisibilityChanged(boolean visible) {
             getUserManagerInternal().onSystemUserVisibilityChanged(visible);
         }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 2897075..5d4f711 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -70,7 +70,6 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 
@@ -172,7 +171,7 @@
             @NonNull AudioSystemAdapter audioSystem) {
         mContext = context;
         mAudioService = service;
-        mBtHelper = new BtHelper(this);
+        mBtHelper = new BtHelper(this, context);
         mDeviceInventory = new AudioDeviceInventory(this);
         mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
         mAudioSystem = audioSystem;
@@ -188,7 +187,7 @@
                       @NonNull AudioSystemAdapter audioSystem) {
         mContext = context;
         mAudioService = service;
-        mBtHelper = new BtHelper(this);
+        mBtHelper = new BtHelper(this, context);
         mDeviceInventory = mockDeviceInventory;
         mSystemServer = mockSystemServer;
         mAudioSystem = audioSystem;
@@ -1392,6 +1391,10 @@
         return mAudioService.hasAudioFocusUsers();
     }
 
+    /*package*/ void postInitSpatializerHeadTrackingSensors() {
+        mAudioService.postInitSpatializerHeadTrackingSensors();
+    }
+
     //---------------------------------------------------------------------
     // Message handling on behalf of helper classes.
     // Each of these methods posts a message to mBrokerHandler message queue.
@@ -1475,6 +1478,15 @@
         sendLMsgNoDelay(MSG_L_RECEIVED_BT_EVENT, SENDMSG_QUEUE, intent);
     }
 
+    /*package*/ void postUpdateLeAudioGroupAddresses(int groupId) {
+        sendIMsgNoDelay(
+                MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES, SENDMSG_QUEUE, groupId);
+    }
+
+    /*package*/ void postSynchronizeLeDevicesInInventory(AdiDeviceState deviceState) {
+        sendLMsgNoDelay(MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState);
+    }
+
     /*package*/ static final class CommunicationDeviceInfo {
         final @NonNull IBinder mCb; // Identifies the requesting client for death handler
         final int mUid; // Requester UID
@@ -1604,6 +1616,14 @@
         }
     }
 
+    /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+        return mBtHelper.getLeAudioDeviceGroupId(device);
+    }
+
+    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+        return mBtHelper.getLeAudioGroupAddresses(groupId);
+    }
+
     /*package*/ void broadcastStickyIntentToCurrentProfileGroup(Intent intent) {
         mSystemServer.broadcastStickyIntentToCurrentProfileGroup(intent);
     }
@@ -1976,6 +1996,22 @@
                         onCheckCommunicationRouteClientState(msg.arg1, msg.arg2 == 1);
                     }
                 } break;
+
+                case MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mDeviceInventory.onUpdateLeAudioGroupAddresses(msg.arg1);
+                        }
+                    } break;
+
+                case MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mDeviceInventory.onSynchronizeLeDevicesInInventory(
+                                    (AdiDeviceState) msg.obj);
+                        }
+                    } break;
+
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -2058,6 +2094,10 @@
     private static final int MSG_L_RECEIVED_BT_EVENT = 55;
 
     private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56;
+    private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
+    private static final int MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY = 58;
+
+
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
@@ -2582,9 +2622,9 @@
         }
     }
 
-    @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
+    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
         synchronized (mDeviceStateLock) {
-            return mDeviceInventory.getDeviceSensorUuid(device);
+            return mDeviceInventory.getDeviceAddresses(device);
         }
     }
 
@@ -2605,15 +2645,19 @@
      * in order to be mocked by a test a the same package
      * (see https://code.google.com/archive/p/mockito/issues/127)
      */
-    public void persistAudioDeviceSettings() {
+    public void postPersistAudioDeviceSettings() {
         sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000);
     }
 
     void onPersistAudioDeviceSettings() {
         final String deviceSettings = mDeviceInventory.getDeviceSettings();
-        Log.v(TAG, "saving AdiDeviceState: " + deviceSettings);
-        final SettingsAdapter settings = mAudioService.getSettings();
-        boolean res = settings.putSecureStringForUser(mAudioService.getContentResolver(),
+        Log.v(TAG, "onPersistAudioDeviceSettings AdiDeviceState: " + deviceSettings);
+        String currentSettings = readDeviceSettings();
+        if (deviceSettings.equals(currentSettings)) {
+            return;
+        }
+        final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+        boolean res = settingsAdapter.putSecureStringForUser(mAudioService.getContentResolver(),
                 Settings.Secure.AUDIO_DEVICE_INVENTORY,
                 deviceSettings, UserHandle.USER_CURRENT);
         if (!res) {
@@ -2621,11 +2665,17 @@
         }
     }
 
+    private String readDeviceSettings() {
+        final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+        final ContentResolver contentResolver = mAudioService.getContentResolver();
+        return settingsAdapter.getSecureStringForUser(contentResolver,
+                Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+    }
+
     void onReadAudioDeviceSettings() {
         final SettingsAdapter settingsAdapter = mAudioService.getSettings();
         final ContentResolver contentResolver = mAudioService.getContentResolver();
-        String settings = settingsAdapter.getSecureStringForUser(contentResolver,
-                Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+        String settings = readDeviceSettings();
         if (settings == null) {
             Log.i(TAG, "reading AdiDeviceState from legacy key"
                     + Settings.Secure.SPATIAL_AUDIO_ENABLED);
@@ -2688,8 +2738,8 @@
     }
 
     @Nullable
-    AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) {
-        return mDeviceInventory.findBtDeviceStateForAddress(address, isBle);
+    AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) {
+        return mDeviceInventory.findBtDeviceStateForAddress(address, deviceType);
     }
 
     //------------------------------------------------
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e59fd77..7ba0827 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -15,14 +15,23 @@
  */
 package com.android.server.audio;
 
+import static android.media.AudioSystem.DEVICE_IN_ALL_SCO_SET;
 import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP_SET;
 import static android.media.AudioSystem.DEVICE_OUT_ALL_BLE_SET;
+import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO_SET;
+import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID;
+import static android.media.AudioSystem.isBluetoothA2dpOutDevice;
 import static android.media.AudioSystem.isBluetoothDevice;
+import static android.media.AudioSystem.isBluetoothLeOutDevice;
+import static android.media.AudioSystem.isBluetoothOutDevice;
+import static android.media.AudioSystem.isBluetoothScoOutDevice;
+
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.content.Intent;
 import android.media.AudioDeviceAttributes;
@@ -72,7 +81,6 @@
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
 import java.util.stream.Stream;
 
 /**
@@ -118,6 +126,7 @@
                 return oldState;
             });
         }
+        mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState);
     }
 
     /**
@@ -125,23 +134,28 @@
      * Bluetooth device and no corresponding entry already exists.
      * @param ada the device to add if needed
      */
-    void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) {
-        if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) {
+    void addAudioDeviceInInventoryIfNeeded(int deviceType, String address, String peerAddres) {
+        if (!isBluetoothOutDevice(deviceType)) {
             return;
         }
         synchronized (mDeviceInventoryLock) {
-            if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) {
+            AdiDeviceState ads = findBtDeviceStateForAddress(address, deviceType);
+            if (ads == null) {
+                ads = findBtDeviceStateForAddress(peerAddres, deviceType);
+            }
+            if (ads != null) {
+                mDeviceBroker.postSynchronizeLeDevicesInInventory(ads);
                 return;
             }
-            AdiDeviceState ads = new AdiDeviceState(
-                    ada.getType(), ada.getInternalType(), ada.getAddress());
+            ads = new AdiDeviceState(AudioDeviceInfo.convertInternalDeviceToDeviceType(deviceType),
+                    deviceType, address);
             mDeviceInventory.put(ads.getDeviceId(), ads);
+            mDeviceBroker.postPersistAudioDeviceSettings();
         }
-        mDeviceBroker.persistAudioDeviceSettings();
     }
 
     /**
-     * Adds a new AdiDeviceState or updates the audio device cateogory of the matching
+     * Adds a new AdiDeviceState or updates the audio device category of the matching
      * AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list.
      * @param deviceState the device to update
      */
@@ -152,6 +166,63 @@
                 return oldState;
             });
         }
+        mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState);
+    }
+
+    /**
+     * synchronize AdiDeviceState for LE devices in the same group
+     */
+    void onSynchronizeLeDevicesInInventory(AdiDeviceState updatedDevice) {
+        synchronized (mDevicesLock) {
+            synchronized (mDeviceInventoryLock) {
+                boolean found = false;
+                for (DeviceInfo di : mConnectedDevices.values()) {
+                    if (di.mDeviceType != updatedDevice.getInternalDeviceType()) {
+                        continue;
+                    }
+                    if (di.mDeviceAddress.equals(updatedDevice.getDeviceAddress())) {
+                        for (AdiDeviceState ads2 : mDeviceInventory.values()) {
+                            if (!(di.mDeviceType == ads2.getInternalDeviceType()
+                                    && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) {
+                                continue;
+                            }
+                            ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                            ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                            ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                            ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
+                            found = true;
+                            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                                    "onSynchronizeLeDevicesInInventory synced device pair ads1="
+                                    + updatedDevice + " ads2=" + ads2).printLog(TAG));
+                            break;
+                        }
+                    }
+                    if (di.mPeerDeviceAddress.equals(updatedDevice.getDeviceAddress())) {
+                        for (AdiDeviceState ads2 : mDeviceInventory.values()) {
+                            if (!(di.mDeviceType == ads2.getInternalDeviceType()
+                                    && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) {
+                                continue;
+                            }
+                            ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                            ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                            ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                            ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
+                            found = true;
+                            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                                    "onSynchronizeLeDevicesInInventory synced device pair ads1="
+                                    + updatedDevice + " peer ads2=" + ads2).printLog(TAG));
+                            break;
+                        }
+                    }
+                    if (found) {
+                        break;
+                    }
+                }
+                if (found) {
+                    mDeviceBroker.postPersistAudioDeviceSettings();
+                }
+            }
+        }
     }
 
     /**
@@ -163,9 +234,21 @@
      * @return the found {@link AdiDeviceState} or {@code null} otherwise.
      */
     @Nullable
-    AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) {
+    AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) {
+        Set<Integer> deviceSet;
+        if (isBluetoothA2dpOutDevice(deviceType)) {
+            deviceSet = DEVICE_OUT_ALL_A2DP_SET;
+        } else if (isBluetoothLeOutDevice(deviceType)) {
+            deviceSet = DEVICE_OUT_ALL_BLE_SET;
+        } else if (isBluetoothScoOutDevice(deviceType)) {
+            deviceSet = DEVICE_OUT_ALL_SCO_SET;
+        } else if (deviceType == DEVICE_OUT_HEARING_AID) {
+            deviceSet = new HashSet<>();
+            deviceSet.add(DEVICE_OUT_HEARING_AID);
+        } else {
+            return null;
+        }
         synchronized (mDeviceInventoryLock) {
-            final Set<Integer> deviceSet = isBle ? DEVICE_OUT_ALL_BLE_SET : DEVICE_OUT_ALL_A2DP_SET;
             for (Integer internalType : deviceSet) {
                 AdiDeviceState deviceState = mDeviceInventory.get(
                         new Pair<>(internalType, address));
@@ -345,7 +428,8 @@
         final @NonNull String mDeviceName;
         final @NonNull String mDeviceAddress;
         int mDeviceCodecFormat;
-        final UUID mSensorUuid;
+        @NonNull String mPeerDeviceAddress;
+        final int mGroupId;
 
         /** Disabled operating modes for this device. Use a negative logic so that by default
          * an empty list means all modes are allowed.
@@ -353,12 +437,13 @@
         @NonNull ArraySet<String> mDisabledModes = new ArraySet(0);
 
         DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat, @Nullable UUID sensorUuid) {
+                   int deviceCodecFormat, String peerDeviceAddress, int groupId) {
             mDeviceType = deviceType;
             mDeviceName = deviceName == null ? "" : deviceName;
             mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
             mDeviceCodecFormat = deviceCodecFormat;
-            mSensorUuid = sensorUuid;
+            mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress;
+            mGroupId = groupId;
         }
 
         void setModeDisabled(String mode) {
@@ -379,7 +464,8 @@
 
         DeviceInfo(int deviceType, String deviceName, String deviceAddress,
                    int deviceCodecFormat) {
-            this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null);
+            this(deviceType, deviceName, deviceAddress, deviceCodecFormat,
+                    null, BluetoothLeAudio.GROUP_ID_INVALID);
         }
 
         DeviceInfo(int deviceType, String deviceName, String deviceAddress) {
@@ -393,7 +479,8 @@
                     + ") name:" + mDeviceName
                     + " addr:" + mDeviceAddress
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
-                    + " sensorUuid: " + Objects.toString(mSensorUuid)
+                    + " peer addr:" + mPeerDeviceAddress
+                    + " group:" + mGroupId
                     + " disabled modes: " + mDisabledModes + "]";
         }
 
@@ -714,6 +801,27 @@
         }
     }
 
+
+    /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) {
+        synchronized (mDevicesLock) {
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                if (di.mGroupId == groupId) {
+                    List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                    if (di.mPeerDeviceAddress.equals("")) {
+                        for (String addr : addresses) {
+                            if (!addr.equals(di.mDeviceAddress)) {
+                                di.mPeerDeviceAddress = addr;
+                                break;
+                            }
+                        }
+                    } else if (!addresses.contains(di.mPeerDeviceAddress)) {
+                        di.mPeerDeviceAddress = "";
+                    }
+                }
+            }
+        }
+    }
+
     /*package*/ void onReportNewRoutes() {
         int n = mRoutesObservers.beginBroadcast();
         if (n > 0) {
@@ -1419,7 +1527,7 @@
                     if (!connect) {
                         purgeDevicesRoles_l();
                     } else {
-                        addAudioDeviceInInventoryIfNeeded(attributes);
+                        addAudioDeviceInInventoryIfNeeded(device, address, "");
                     }
                 }
                 mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
@@ -1477,7 +1585,7 @@
             final ArraySet<String> toRemove = new ArraySet<>();
             // Disconnect ALL DEVICE_OUT_HEARING_AID devices
             mConnectedDevices.values().forEach(deviceInfo -> {
-                if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                if (deviceInfo.mDeviceType == DEVICE_OUT_HEARING_AID) {
                     toRemove.add(deviceInfo.mDeviceAddress);
                 }
             });
@@ -1485,8 +1593,8 @@
                     .set(MediaMetrics.Property.EVENT, "disconnectHearingAid")
                     .record();
             if (toRemove.size() > 0) {
-                final int delay = checkSendBecomingNoisyIntentInt(
-                        AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE);
+                final int delay = checkSendBecomingNoisyIntentInt(DEVICE_OUT_HEARING_AID,
+                        AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE);
                 toRemove.stream().forEach(deviceAddress ->
                         // TODO delay not used?
                         makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/)
@@ -1687,12 +1795,8 @@
         // Reset A2DP suspend state each time a new sink is connected
         mDeviceBroker.clearA2dpSuspended(true /* internalOnly */);
 
-        // The convention for head tracking sensors associated with A2DP devices is to
-        // use a UUID derived from the MAC address as follows:
-        //   time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address
-        UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
         final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
-                address, codec, sensorUuid);
+                address, codec);
         final String diKey = di.getKey();
         mConnectedDevices.put(diKey, di);
         // on a connection always overwrite the device seen by AudioPolicy, see comment above when
@@ -1703,7 +1807,7 @@
         setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
 
         updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/);
-        addAudioDeviceInInventoryIfNeeded(ada);
+        addAudioDeviceInInventoryIfNeeded(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, "");
     }
 
     static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER,
@@ -1723,15 +1827,15 @@
             return;
         }
         DeviceInfo leOutDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_BLE_SET);
         DeviceInfo leInDevice =
                 getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET);
         DeviceInfo a2dpDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET);
         DeviceInfo scoOutDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET);
         DeviceInfo scoInDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET);
         boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled());
         boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled())
                 || (leInDevice != null && leInDevice.isDuplexModeEnabled());
@@ -1765,7 +1869,7 @@
                 continue;
             }
 
-            if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) {
+            if (isBluetoothOutDevice(di.mDeviceType)) {
                 for (AudioProductStrategy strategy : mStrategies) {
                     boolean disable = false;
                     if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) {
@@ -1832,23 +1936,20 @@
     int checkProfileIsConnected(int profile) {
         switch (profile) {
             case BluetoothProfile.HEADSET:
-                if (getFirstConnectedDeviceOfTypes(
-                        AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null
-                        || getFirstConnectedDeviceOfTypes(
-                                AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) {
+                if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET) != null
+                        || getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET) != null) {
                     return profile;
                 }
                 break;
             case BluetoothProfile.A2DP:
-                if (getFirstConnectedDeviceOfTypes(
-                        AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) {
+                if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET) != null) {
                     return profile;
                 }
                 break;
             case BluetoothProfile.LE_AUDIO:
             case BluetoothProfile.LE_AUDIO_BROADCAST:
                 if (getFirstConnectedDeviceOfTypes(
-                        AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null
+                        DEVICE_OUT_ALL_BLE_SET) != null
                         || getFirstConnectedDeviceOfTypes(
                                 AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) {
                     return profile;
@@ -2006,28 +2107,28 @@
     private void makeHearingAidDeviceAvailable(
             String address, String name, int streamType, String eventSource) {
         final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
-                AudioSystem.DEVICE_OUT_HEARING_AID);
+                DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
 
         mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource);
 
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_HEARING_AID, address, name);
+                DEVICE_OUT_HEARING_AID, address, name);
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.put(
-                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address),
-                new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address));
-        mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID);
+                DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address),
+                new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address));
+        mDeviceBroker.postAccessoryPlugMediaUnmute(DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postApplyVolumeOnDevice(streamType,
-                AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
+                DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
         setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/);
-        addAudioDeviceInInventoryIfNeeded(ada);
+        addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_HEARING_AID, address, "");
         new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
                 .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
                 .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                        AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
                 .set(MediaMetrics.Property.NAME, name)
                 .set(MediaMetrics.Property.STREAM_TYPE,
                         AudioSystem.streamToString(streamType))
@@ -2037,18 +2138,18 @@
     @GuardedBy("mDevicesLock")
     private void makeHearingAidDeviceUnavailable(String address) {
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_HEARING_AID, address);
+                DEVICE_OUT_HEARING_AID, address);
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.remove(
-                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address));
+                DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
         // Remove Hearing Aid routes as well
         setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/);
         new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceUnavailable")
                 .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
                 .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                        AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
                 .record();
         mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
     }
@@ -2060,7 +2161,7 @@
      */
     boolean isHearingAidConnected() {
         return getFirstConnectedDeviceOfTypes(
-                Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null;
+                Sets.newHashSet(DEVICE_OUT_HEARING_AID)) != null;
     }
 
     /**
@@ -2102,6 +2203,20 @@
             final String address = btInfo.mDevice.getAddress();
             String name = BtHelper.getName(btInfo.mDevice);
 
+            // Find LE Group ID and peer headset address if available
+            final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice);
+            String peerAddress = "";
+            if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+                List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                if (addresses.size() > 1) {
+                    for (String addr : addresses) {
+                        if (!addr.equals(address)) {
+                            peerAddress = addr;
+                            break;
+                        }
+                    }
+                }
+            }
             // The BT Stack does not provide a name for LE Broadcast devices
             if (device == AudioSystem.DEVICE_OUT_BLE_BROADCAST && name.equals("")) {
                 name = "Broadcast";
@@ -2127,14 +2242,12 @@
             }
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
-
-            UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
             mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
                     new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT,
-                            sensorUuid));
+                            peerAddress, groupId));
             mDeviceBroker.postAccessoryPlugMediaUnmute(device);
             setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
-            addAudioDeviceInInventoryIfNeeded(ada);
+            addAudioDeviceInInventoryIfNeeded(device, address, peerAddress);
         }
 
         if (streamType == AudioSystem.STREAM_DEFAULT) {
@@ -2226,12 +2339,12 @@
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE);
-        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(DEVICE_OUT_HEARING_AID);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_HEADSET);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_BROADCAST);
-        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_A2DP_SET);
         BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
-        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_BLE_SET);
     }
 
     // must be called before removing the device from mConnectedDevices
@@ -2512,16 +2625,22 @@
         mDevRoleCapturePresetDispatchers.finishBroadcast();
     }
 
-    @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
+    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+        List<String> addresses = new ArrayList<String>();
         final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(),
                 device.getAddress());
         synchronized (mDevicesLock) {
             DeviceInfo di = mConnectedDevices.get(key);
-            if (di == null) {
-                return null;
+            if (di != null) {
+                if (!di.mDeviceAddress.isEmpty()) {
+                    addresses.add(di.mDeviceAddress);
+                }
+                if (!di.mPeerDeviceAddress.isEmpty()) {
+                    addresses.add(di.mPeerDeviceAddress);
+                }
             }
-            return di.mSensorUuid;
         }
+        return addresses;
     }
 
     /*package*/ String getDeviceSettings() {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b209fb0..99321c4 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -38,6 +38,7 @@
 import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE;
 
 import static com.android.media.audio.Flags.bluetoothMacAddressAnonymization;
+import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
@@ -236,7 +237,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.UUID;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -1371,19 +1371,21 @@
                 sRingerAndZenModeMutedStreams, "onInitStreamsAndVolumes"));
         setRingerModeInt(getRingerModeInternal(), false);
 
-        final float[] preScale = new float[3];
-        preScale[0] = mContext.getResources().getFraction(
-                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1,
-                1, 1);
-        preScale[1] = mContext.getResources().getFraction(
-                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2,
-                1, 1);
-        preScale[2] = mContext.getResources().getFraction(
-                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3,
-                1, 1);
-        for (int i = 0; i < preScale.length; i++) {
-            if (0.0f <= preScale[i] && preScale[i] <= 1.0f) {
-                mPrescaleAbsoluteVolume[i] = preScale[i];
+        if (!disablePrescaleAbsoluteVolume()) {
+            final float[] preScale = new float[3];
+            preScale[0] = mContext.getResources().getFraction(
+                    com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1,
+                    1, 1);
+            preScale[1] = mContext.getResources().getFraction(
+                    com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2,
+                    1, 1);
+            preScale[2] = mContext.getResources().getFraction(
+                    com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3,
+                    1, 1);
+            for (int i = 0; i < preScale.length; i++) {
+                if (0.0f <= preScale[i] && preScale[i] <= 1.0f) {
+                    mPrescaleAbsoluteVolume[i] = preScale[i];
+                }
             }
         }
 
@@ -8618,7 +8620,7 @@
             if (index == 0) {
                 // 0% for volume 0
                 index = 0;
-            } else if (index > 0 && index <= 3) {
+            } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) {
                 // Pre-scale for volume steps 1 2 and 3
                 index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10;
             } else {
@@ -11051,7 +11053,9 @@
 
         final String addr = Objects.requireNonNull(address);
 
-        AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr, isBle);
+        AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr,
+                (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET
+                        : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
 
         int internalType = !isBle ? DEVICE_OUT_BLUETOOTH_A2DP
                 : ((btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES)
@@ -11067,7 +11071,7 @@
         deviceState.setAudioDeviceCategory(btAudioDeviceCategory);
 
         mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState);
-        mDeviceBroker.persistAudioDeviceSettings();
+        mDeviceBroker.postPersistAudioDeviceSettings();
 
         mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
         mSoundDoseHelper.setAudioDeviceCategory(addr, internalType,
@@ -11081,7 +11085,8 @@
         super.getBluetoothAudioDeviceCategory_enforcePermission();
 
         final AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(
-                Objects.requireNonNull(address), isBle);
+                Objects.requireNonNull(address), (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET
+                        : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
         if (deviceState == null) {
             return AUDIO_DEVICE_CATEGORY_UNKNOWN;
         }
@@ -11448,6 +11453,14 @@
         pw.print("  adjust-only absolute volume devices="); pw.println(dumpDeviceTypes(
                 getAbsoluteVolumeDevicesWithBehavior(
                         AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY)));
+        pw.print("  pre-scale for bluetooth absolute volume ");
+        if (disablePrescaleAbsoluteVolume()) {
+            pw.println("= disabled");
+        } else {
+            pw.println("=" + mPrescaleAbsoluteVolume[0]
+                    + ", " + mPrescaleAbsoluteVolume[1]
+                    + ", " + mPrescaleAbsoluteVolume[2]);
+        }
         pw.print("  mExtVolumeController="); pw.println(mExtVolumeController);
         pw.print("  mHdmiAudioSystemClient="); pw.println(mHdmiAudioSystemClient);
         pw.print("  mHdmiPlaybackClient="); pw.println(mHdmiPlaybackClient);
@@ -13585,8 +13598,8 @@
         return activeAssistantUids;
     }
 
-    UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
-        return mDeviceBroker.getDeviceSensorUuid(device);
+    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+        return mDeviceBroker.getDeviceAddresses(device);
     }
 
     //======================
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index cce6bd2..7b96215 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -26,7 +26,9 @@
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothLeAudioCodecStatus;
 import android.bluetooth.BluetoothProfile;
+import android.content.Context;
 import android.content.Intent;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
@@ -43,6 +45,7 @@
 import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -57,9 +60,11 @@
     private static final String TAG = "AS.BtHelper";
 
     private final @NonNull AudioDeviceBroker mDeviceBroker;
+    private final @NonNull Context mContext;
 
-    BtHelper(@NonNull AudioDeviceBroker broker) {
+    BtHelper(@NonNull AudioDeviceBroker broker, Context context) {
         mDeviceBroker = broker;
+        mContext = context;
     }
 
     // BluetoothHeadset API to control SCO connection
@@ -498,6 +503,32 @@
         }
     }
 
+    // BluetoothLeAudio callback used to update the list of addresses in the same group as a
+    // connected LE Audio device
+    MyLeAudioCallback mLeAudioCallback = null;
+
+    class MyLeAudioCallback implements BluetoothLeAudio.Callback {
+        @Override
+        public void onCodecConfigChanged(int groupId,
+                                  @NonNull BluetoothLeAudioCodecStatus status) {
+            // Do nothing
+        }
+
+        @Override
+        public void onGroupNodeAdded(@NonNull BluetoothDevice device, int groupId) {
+            mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+        }
+
+        @Override
+        public void onGroupNodeRemoved(@NonNull BluetoothDevice device, int groupId) {
+            mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+        }
+        @Override
+        public void onGroupStatusChanged(int groupId, int groupStatus) {
+            mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+        }
+    }
+
     // @GuardedBy("mDeviceBroker.mSetModeLock")
     @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
     /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
@@ -519,6 +550,11 @@
                 mHearingAid = (BluetoothHearingAid) proxy;
                 break;
             case BluetoothProfile.LE_AUDIO:
+                if (mLeAudio == null) {
+                    mLeAudioCallback = new MyLeAudioCallback();
+                    ((BluetoothLeAudio) proxy).registerCallback(
+                            mContext.getMainExecutor(), mLeAudioCallback);
+                }
                 mLeAudio = (BluetoothLeAudio) proxy;
                 break;
             case BluetoothProfile.A2DP_SINK:
@@ -977,6 +1013,28 @@
         return result;
     }
 
+    /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+        if (mLeAudio == null || device == null) {
+            return BluetoothLeAudio.GROUP_ID_INVALID;
+        }
+        return mLeAudio.getGroupId(device);
+    }
+
+    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+        List<String> addresses = new ArrayList<String>();
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter == null || mLeAudio == null) {
+            return addresses;
+        }
+        List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
+        for (BluetoothDevice device : activeDevices) {
+            if (device != null && mLeAudio.getGroupId(device) == groupId) {
+                addresses.add(device.getAddress());
+            }
+        }
+        return addresses;
+    }
+
     /**
      * Returns the String equivalent of the btCodecType.
      *
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 7abd9c7..ea92154 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -564,7 +564,7 @@
         }
         if (updatedDevice != null) {
             onRoutingUpdated();
-            mDeviceBroker.persistAudioDeviceSettings();
+            mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(updatedDevice, "addCompatibleAudioDevice");
         }
     }
@@ -614,7 +614,7 @@
         if (deviceState != null && deviceState.isSAEnabled()) {
             deviceState.setSAEnabled(false);
             onRoutingUpdated();
-            mDeviceBroker.persistAudioDeviceSettings();
+            mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(deviceState, "removeCompatibleAudioDevice");
         }
     }
@@ -716,7 +716,7 @@
                             ada.getAddress());
             initSAState(deviceState);
             mDeviceBroker.addOrUpdateDeviceSAStateInInventory(deviceState);
-            mDeviceBroker.persistAudioDeviceSettings();
+            mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later.
         }
     }
@@ -1206,7 +1206,7 @@
         }
         Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
         deviceState.setHeadTrackerEnabled(enabled);
-        mDeviceBroker.persistAudioDeviceSettings();
+        mDeviceBroker.postPersistAudioDeviceSettings();
         logDeviceState(deviceState, "setHeadTrackerEnabled");
 
         // check current routing to see if it affects the headtracking mode
@@ -1248,7 +1248,7 @@
         if (deviceState != null) {
             if (!deviceState.hasHeadTracker()) {
                 deviceState.setHasHeadTracker(true);
-                mDeviceBroker.persistAudioDeviceSettings();
+                mDeviceBroker.postPersistAudioDeviceSettings();
                 logDeviceState(deviceState, "setHasHeadTracker");
             }
             return deviceState.isHeadTrackerEnabled();
@@ -1631,25 +1631,33 @@
             return headHandle;
         }
         final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
-        UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice);
+        List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
+
         // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
         // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
         // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by
         // SensorPoseProvider).
         // Note: this is a dynamic sensor list right now.
         List<Sensor> sensors = mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER);
-        for (Sensor sensor : sensors) {
-            final UUID uuid = sensor.getUuid();
-            if (uuid.equals(routingDeviceUuid)) {
-                headHandle = sensor.getHandle();
-                if (!setHasHeadTracker(currentDevice)) {
-                    headHandle = -1;
+        for (String address : deviceAddresses) {
+            UUID routingDeviceUuid = UuidUtils.uuidFromAudioDeviceAttributes(
+                    new AudioDeviceAttributes(currentDevice.getInternalType(), address));
+            for (Sensor sensor : sensors) {
+                final UUID uuid = sensor.getUuid();
+                if (uuid.equals(routingDeviceUuid)) {
+                    headHandle = sensor.getHandle();
+                    if (!setHasHeadTracker(currentDevice)) {
+                        headHandle = -1;
+                    }
+                    break;
                 }
-                break;
+                if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                    headHandle = sensor.getHandle();
+                    // we do not break, perhaps we find a head tracker on device.
+                }
             }
-            if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
-                headHandle = sensor.getHandle();
-                // we do not break, perhaps we find a head tracker on device.
+            if (headHandle != -1) {
+                break;
             }
         }
         return headHandle;
diff --git a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
index ab480e8..0814375 100644
--- a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
+++ b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java
@@ -31,6 +31,7 @@
 import com.android.server.criticalevents.nano.CriticalEventProto.HalfWatchdog;
 import com.android.server.criticalevents.nano.CriticalEventProto.JavaCrash;
 import com.android.server.criticalevents.nano.CriticalEventProto.NativeCrash;
+import com.android.server.criticalevents.nano.CriticalEventProto.SystemServerStarted;
 import com.android.server.criticalevents.nano.CriticalEventProto.Watchdog;
 
 import java.io.File;
@@ -141,6 +142,13 @@
         return System.currentTimeMillis();
     }
 
+    /** Logs when system server started. */
+    public void logSystemServerStarted() {
+        CriticalEventProto event = new CriticalEventProto();
+        event.setSystemServerStarted(new SystemServerStarted());
+        log(event);
+    }
+
     /** Logs a watchdog. */
     public void logWatchdog(String subject, UUID uuid) {
         Watchdog watchdog = new Watchdog();
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index d6580df..d3eedd7 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1326,7 +1326,10 @@
         removeAction(NewDeviceAction.class);
         removeAction(AbsoluteVolumeAudioStatusAction.class);
 
-        disableSystemAudioIfExist();
+        // Keep SAM enabled if eARC is enabled, unless we're going to Standby.
+        if (initiatedByCec || !mService.isEarcEnabled()){
+            disableSystemAudioIfExist();
+        }
         disableArcIfExist();
 
         super.disableDevice(initiatedByCec, callback);
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index c28a68b..b3926fd 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -3610,7 +3610,7 @@
         }
     }
 
-    private boolean isEarcEnabled() {
+    public boolean isEarcEnabled() {
         synchronized (mLock) {
             return mEarcEnabled;
         }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a52870e..f8d27f1 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -476,20 +476,30 @@
             pkgSetting.setLoadingProgress(1f);
         }
 
+        // TODO: passes the package name as an argument in a message to the handler for V+
+        //  so we don't need to rely on creating lambda objects so frequently.
+        if (UpdateOwnershipHelper.hasValidOwnershipDenyList(pkgSetting)) {
+            mPm.mHandler.post(() -> handleUpdateOwnerDenyList(pkgSetting));
+        }
+        return pkg;
+    }
+
+    private void handleUpdateOwnerDenyList(PackageSetting pkgSetting) {
         ArraySet<String> listItems = mUpdateOwnershipHelper.readUpdateOwnerDenyList(pkgSetting);
         if (listItems != null && !listItems.isEmpty()) {
-            mUpdateOwnershipHelper.addToUpdateOwnerDenyList(pkgSetting.getPackageName(), listItems);
-            for (String unownedPackage : listItems) {
-                PackageSetting unownedSetting = mPm.mSettings.getPackageLPr(unownedPackage);
-                SystemConfig config = SystemConfig.getInstance();
-                if (unownedSetting != null
-                        && config.getSystemAppUpdateOwnerPackageName(unownedPackage) == null) {
-                    unownedSetting.setUpdateOwnerPackage(null);
+            mUpdateOwnershipHelper.addToUpdateOwnerDenyList(pkgSetting.getPackageName(),
+                    listItems);
+            SystemConfig config = SystemConfig.getInstance();
+            synchronized (mPm.mLock) {
+                for (String unownedPackage : listItems) {
+                    PackageSetting unownedSetting = mPm.mSettings.getPackageLPr(unownedPackage);
+                    if (unownedSetting != null
+                            && config.getSystemAppUpdateOwnerPackageName(unownedPackage) == null) {
+                        unownedSetting.setUpdateOwnerPackage(null);
+                    }
                 }
             }
         }
-
-        return pkg;
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java b/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java
index 43752f3..adac68b 100644
--- a/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java
+++ b/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java
@@ -48,7 +48,7 @@
 
     private final Object mLock = new Object();
 
-    private static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) {
+    static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) {
         AndroidPackage pkg = pkgSetting.getPkg();
         // we're checking for uses-permission for these priv permissions instead of grant as we're
         // only considering system apps to begin with, so presumed to be granted.
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 97420d0..c1da589 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -1281,12 +1281,19 @@
 
     private void addBytesTransferByTagAndMeteredAtoms(@NonNull NetworkStatsExt statsExt,
             @NonNull List<StatsEvent> pulledData) {
+        // Workaround for 5G NSA mode, see {@link NetworkStatsManager#NETWORK_TYPE_5G_NSA}.
+        // 5G NSA mode means the primary cell is LTE with a secondary connection to an
+        // NR cell. To mitigate risk, NetworkStats is currently storing this state as
+        // a fake RAT type rather than storing the boolean separately.
+        final boolean is5GNsa = statsExt.ratType == NetworkStatsManager.NETWORK_TYPE_5G_NSA;
+
         for (NetworkStats.Entry entry : statsExt.stats) {
             pulledData.add(FrameworkStatsLog.buildStatsEvent(
                     FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED, entry.getUid(),
                     entry.getMetered() == NetworkStats.METERED_YES, entry.getTag(),
                     entry.getRxBytes(), entry.getRxPackets(), entry.getTxBytes(),
-                    entry.getTxPackets()));
+                    entry.getTxPackets(),
+                    is5GNsa ? TelephonyManager.NETWORK_TYPE_LTE : statsExt.ratType));
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f0698be..f462efc 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -125,7 +125,6 @@
 import static com.android.server.wm.Task.REPARENT_KEEP_ROOT_TASK_AT_FRONT;
 import static com.android.server.wm.WindowManagerService.MY_PID;
 import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
-import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext;
 
 import android.Manifest;
 import android.annotation.IntDef;
@@ -166,7 +165,6 @@
 import android.app.assist.AssistContent;
 import android.app.assist.AssistStructure;
 import android.app.compat.CompatChanges;
-import android.app.sdksandbox.sandboxactivity.SdkSandboxActivityAuthority;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -1260,13 +1258,6 @@
                 true /*validateIncomingUser*/);
     }
 
-    static boolean isSdkSandboxActivity(Context context, Intent intent) {
-        return intent != null
-                && (sandboxActivitySdkBasedContext()
-                        ? SdkSandboxActivityAuthority.isSdkSandboxActivity(context, intent)
-                        : intent.isSandboxActivity(context));
-    }
-
     private int startActivityAsUser(IApplicationThread caller, String callingPackage,
             @Nullable String callingFeatureId, Intent intent, String resolvedType,
             IBinder resultTo, String resultWho, int requestCode, int startFlags,
@@ -1277,7 +1268,7 @@
         assertPackageMatchesCallingUid(callingPackage);
         enforceNotIsolatedCaller("startActivityAsUser");
 
-        if (isSdkSandboxActivity(mContext, intent)) {
+        if (intent != null && intent.isSandboxActivity(mContext)) {
             SdkSandboxManagerLocal sdkSandboxManagerLocal = LocalManagerRegistry.getManager(
                     SdkSandboxManagerLocal.class);
             sdkSandboxManagerLocal.enforceAllowedToHostSandboxedActivity(
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index e5eb303..777b5cd 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1089,7 +1089,7 @@
             // Remove the process record so it won't be considered as alive.
             mService.mProcessNames.remove(wpc.mName, wpc.mUid);
             mService.mProcessMap.remove(wpc.getPid());
-        } else if (ActivityTaskManagerService.isSdkSandboxActivity(mService.mContext, r.intent)) {
+        } else if (r.intent.isSandboxActivity(mService.mContext)) {
             Slog.e(TAG, "Abort sandbox activity launching as no sandbox process to host it.");
             r.finishIfPossible("No sandbox process for the activity", false /* oomAdj */);
             r.launchFailed = true;
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index cdaab46..2f9ef50 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -30,6 +30,7 @@
 import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY;
 import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel;
 import static com.android.window.flags.Flags.balShowToasts;
+import static com.android.window.flags.Flags.balShowToastsBlocked;
 import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -321,6 +322,7 @@
                     .append(getDebugPackageName(mCallingPackage, mCallingUid));
             sb.append("; callingUid: ").append(mCallingUid);
             sb.append("; callingPid: ").append(mCallingPid);
+            sb.append("; isPendingIntent: ").append(isPendingIntent());
             sb.append("; appSwitchState: ").append(mAppSwitchState);
             sb.append("; callingUidHasAnyVisibleWindow: ").append(mCallingUidHasAnyVisibleWindow);
             sb.append("; callingUidProcState: ").append(DebugUtils.valueToString(
@@ -328,7 +330,7 @@
             sb.append("; isCallingUidPersistentSystemProcess: ")
                     .append(mIsCallingUidPersistentSystemProcess);
             sb.append("; balAllowedByPiCreator: ").append(mBalAllowedByPiCreator);
-            if (!isPendingIntent()) {
+            if (isPendingIntent()) {
                 sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender);
                 sb.append("; realCallingPackage: ")
                         .append(getDebugPackageName(mRealCallingPackage, mRealCallingUid));
@@ -340,16 +342,18 @@
                         ActivityManager.class, "PROCESS_STATE_", mRealCallingUidProcState));
                 sb.append("; isRealCallingUidPersistentSystemProcess: ")
                         .append(mIsRealCallingUidPersistentSystemProcess);
+                sb.append("; originatingPendingIntent: ").append(mOriginatingPendingIntent);
             }
-            sb.append("; originatingPendingIntent: ").append(mOriginatingPendingIntent);
             sb.append("; backgroundStartPrivileges: ").append(mBackgroundStartPrivileges);
             sb.append("; intent: ").append(mIntent);
             sb.append("; callerApp: ").append(mCallerApp);
-            sb.append("; realCallerApp: ").append(mRealCallerApp);
+            if (isPendingIntent()) {
+                sb.append("; realCallerApp: ").append(mRealCallerApp);
+            }
             if (mCallerApp != null) {
                 sb.append("; inVisibleTask: ").append(mCallerApp.hasActivityInVisibleTask());
             }
-            if (!isPendingIntent()) {
+            if (isPendingIntent()) {
                 if (mRealCallerApp != null) {
                     sb.append("; realInVisibleTask: ")
                             .append(mRealCallerApp.hasActivityInVisibleTask());
@@ -469,7 +473,7 @@
             // anything that has fallen through would currently be aborted
             Slog.w(TAG, "Background activity launch blocked! "
                     + state.dump(resultForCaller));
-            showBalToast("BAL blocked", state);
+            showBalBlockedToast("BAL blocked", state);
             return statsLog(BalVerdict.BLOCK, state);
         }
 
@@ -509,10 +513,12 @@
                 == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
             // Both caller and real caller allow with system defined behavior
             Slog.wtf(TAG,
-                    "With Android 15 BAL hardening this activity start would be blocked"
+                    "With Android 15 BAL hardening this activity start may be blocked"
+                            + " if the PI creator upgrades target_sdk to 35+"
+                            + " AND the PI sender upgrades target_sdk to 34+! "
                             + " (missing opt in by PI creator)! "
                             + state.dump(resultForCaller, resultForRealCaller));
-            showBalToast("BAL would be blocked", state);
+            showBalRiskToast("BAL would be blocked", state);
             // return the realCaller result for backwards compatibility
             return statsLog(resultForRealCaller, state);
         }
@@ -521,10 +527,11 @@
                 == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
             // Allowed before V by creator
             Slog.wtf(TAG,
-                    "With Android 15 BAL hardening this activity start would be blocked"
+                    "With Android 15 BAL hardening this activity start may be blocked"
+                            + " if the PI creator upgrades target_sdk to 35+! "
                             + " (missing opt in by PI creator)! "
                             + state.dump(resultForCaller, resultForRealCaller));
-            showBalToast("BAL would be blocked", state);
+            showBalRiskToast("BAL would be blocked", state);
             return statsLog(resultForCaller, state);
         }
         if (resultForRealCaller.allows()
@@ -533,10 +540,11 @@
             // Allowed before U by sender
             if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) {
                 Slog.wtf(TAG,
-                        "With Android 14 BAL hardening this activity start would be blocked"
+                        "With Android 14 BAL hardening this activity start will be blocked"
+                                + " if the PI sender upgrades target_sdk to 34+! "
                                 + " (missing opt in by PI sender)! "
                                 + state.dump(resultForCaller, resultForRealCaller));
-                showBalToast("BAL would be blocked", state);
+                showBalBlockedToast("BAL would be blocked", state);
                 return statsLog(resultForRealCaller, state);
             }
             Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
@@ -547,7 +555,7 @@
         // anything that has fallen through would currently be aborted
         Slog.w(TAG, "Background activity launch blocked! "
                 + state.dump(resultForCaller, resultForRealCaller));
-        showBalToast("BAL blocked", state);
+        showBalBlockedToast("BAL blocked", state);
         return statsLog(BalVerdict.BLOCK, state);
     }
 
@@ -922,7 +930,15 @@
         return true;
     }
 
-    private void showBalToast(String toastText, BalState state) {
+    private void showBalBlockedToast(String toastText, BalState state) {
+        if (balShowToastsBlocked()) {
+            showToast(toastText
+                    + " caller:" + state.mCallingPackage
+                    + " realCaller:" + state.mRealCallingPackage);
+        }
+    }
+
+    private void showBalRiskToast(String toastText, BalState state) {
         if (balShowToasts()) {
             showToast(toastText
                     + " caller:" + state.mCallingPackage
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index c716879..7f80807 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2436,13 +2436,14 @@
         // AppBounds at the root level should mirror the app screen size.
         outConfig.windowConfiguration.setAppBounds(info.mNonDecorFrame);
         outConfig.windowConfiguration.setRotation(rotation);
-        outConfig.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
 
         final float density = mDisplayMetrics.density;
         outConfig.screenWidthDp = (int) (info.mConfigFrame.width() / density + 0.5f);
         outConfig.screenHeightDp = (int) (info.mConfigFrame.height() / density + 0.5f);
         outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale);
         outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale);
+        outConfig.orientation = (outConfig.screenWidthDp <= outConfig.screenHeightDp)
+                ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
         outConfig.screenLayout = computeScreenLayout(
                 Configuration.resetScreenLayout(outConfig.screenLayout),
                 outConfig.screenWidthDp, outConfig.screenHeightDp);
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 00f2b89..2f52de4 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -274,11 +274,6 @@
     boolean mClearedForReorderActivityToFront;
 
     /**
-     * Whether the TaskFragment surface is managed by a system {@link TaskFragmentOrganizer}.
-     */
-    boolean mIsSurfaceManagedBySystemOrganizer = false;
-
-    /**
      * When we are in the process of pausing an activity, before starting the
      * next one, this variable holds the activity that is currently being paused.
      *
@@ -453,21 +448,13 @@
 
     void setTaskFragmentOrganizer(@NonNull TaskFragmentOrganizerToken organizer, int uid,
             @NonNull String processName) {
-        setTaskFragmentOrganizer(organizer, uid, processName,
-                false /* isSurfaceManagedBySystemOrganizer */);
-    }
-
-    void setTaskFragmentOrganizer(@NonNull TaskFragmentOrganizerToken organizer, int uid,
-            @NonNull String processName, boolean isSurfaceManagedBySystemOrganizer) {
         mTaskFragmentOrganizer = ITaskFragmentOrganizer.Stub.asInterface(organizer.asBinder());
         mTaskFragmentOrganizerUid = uid;
         mTaskFragmentOrganizerProcessName = processName;
-        mIsSurfaceManagedBySystemOrganizer = isSurfaceManagedBySystemOrganizer;
     }
 
     void onTaskFragmentOrganizerRemoved() {
         mTaskFragmentOrganizer = null;
-        mIsSurfaceManagedBySystemOrganizer = false;
     }
 
     /** Whether this TaskFragment is organized by the given {@code organizer}. */
@@ -2454,9 +2441,6 @@
         if (mDelayOrganizedTaskFragmentSurfaceUpdate || mTaskFragmentOrganizer == null) {
             return;
         }
-        if (mIsSurfaceManagedBySystemOrganizer) {
-            return;
-        }
         if (mTransitionController.isShellTransitionsEnabled()
                 && !mTransitionController.isCollecting(this)) {
             // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 95e2515..89d47bc 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -2122,8 +2122,7 @@
         // actions.
         TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer();
         taskFragment.setTaskFragmentOrganizer(organizerToken,
-                ownerActivity.getUid(), ownerActivity.info.processName,
-                mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken.asBinder()));
+                ownerActivity.getUid(), ownerActivity.info.processName);
         final int position;
         if (creationParams.getPairedPrimaryFragmentToken() != null) {
             // When there is a paired primary TaskFragment, we want to place the new TaskFragment
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 93dc219..34d6755 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -2008,6 +2008,10 @@
         DeviceManagementResourcesProvider getDeviceManagementResourcesProvider() {
             return new DeviceManagementResourcesProvider();
         }
+
+        boolean isAdminInstalledCaCertAutoApproved() {
+            return false;
+        }
     }
 
     /**
@@ -6158,6 +6162,18 @@
                     .setAdmin(caller.getPackageName())
                     .setBoolean(/* isDelegate */ admin == null)
                     .write();
+
+            if (mInjector.isAdminInstalledCaCertAutoApproved()
+                    && installedAlias != null
+                    && admin != null) {
+                // If device admin called this, approve cert to avoid notifications
+                Slogf.i(LOG_TAG, "Approving admin installed cert");
+                approveCaCert(
+                        installedAlias,
+                        caller.getUserId(),
+                        /* approved */ true);
+            }
+
             return installedAlias;
         });
 
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 56e385d..0a2e806 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -129,6 +129,7 @@
 import com.android.server.contentcapture.ContentCaptureManagerInternal;
 import com.android.server.coverage.CoverageService;
 import com.android.server.cpu.CpuMonitorService;
+import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.devicepolicy.DevicePolicyManagerService;
 import com.android.server.devicestate.DeviceStateManagerService;
 import com.android.server.display.DisplayManagerService;
@@ -964,6 +965,7 @@
             // Only update the timeout after starting all the services so that we use
             // the default timeout to start system server.
             updateWatchdogTimeout(t);
+            CriticalEventLog.getInstance().logSystemServerStarted();
         } catch (Throwable ex) {
             Slog.e("System", "******************************************");
             Slog.e("System", "************ Failure starting system services", ex);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
index c88d6e4..612a091 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
@@ -29,6 +29,9 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableContext;
 import android.util.DebugUtils;
 import android.view.InputDevice;
@@ -40,6 +43,7 @@
 
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
+import com.android.server.accessibility.Flags;
 import com.android.server.accessibility.utils.TouchEventGenerator;
 
 import org.junit.After;
@@ -59,20 +63,29 @@
 @RunWith(AndroidJUnit4.class)
 public class WindowMagnificationGestureHandlerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     public static final int STATE_IDLE = 1;
     public static final int STATE_SHOW_MAGNIFIER_SHORTCUT = 2;
     public static final int STATE_TWO_FINGERS_DOWN = 3;
     public static final int STATE_SHOW_MAGNIFIER_TRIPLE_TAP = 4;
     public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 5;
     public static final int STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 6;
+    public static final int STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP = 7;
+    public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD = 8;
+    public static final int STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD = 9;
     //TODO: Test it after can injecting Handler to GestureMatcher is available.
 
     public static final int FIRST_STATE = STATE_IDLE;
     public static final int LAST_STATE = STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD;
+    public static final int LAST_STATE_WITH_MULTI_FINGER =
+            STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD;
 
     // Co-prime x and y, to potentially catch x-y-swapped errors
     public static final float DEFAULT_TAP_X = 301;
     public static final float DEFAULT_TAP_Y = 299;
+    public static final PointF DEFAULT_POINT = new PointF(DEFAULT_TAP_X, DEFAULT_TAP_Y);
     private static final int DISPLAY_0 = MockWindowMagnificationConnection.TEST_DISPLAY;
 
     @Rule
@@ -141,7 +154,22 @@
                 throw new AssertionError("Failed while testing state " + stateToString(state),
                         e);
             }
-        });
+        }, LAST_STATE);
+    }
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
+    public void testEachState_enabledMultiFinger_isReachableAndRecoverable() {
+        forEachState(state -> {
+            goFromStateIdleTo(state);
+            assertIn(state);
+            returnToNormalFrom(state);
+            try {
+                assertIn(STATE_IDLE);
+            } catch (AssertionError e) {
+                throw new AssertionError("Failed while testing state " + stateToString(state),
+                        e);
+            }
+        }, LAST_STATE_WITH_MULTI_FINGER);
     }
 
     @Test
@@ -159,8 +187,29 @@
                         returnToNormalFrom(state1);
                     }
                 }
-            });
-        });
+            }, LAST_STATE);
+        }, LAST_STATE);
+    }
+
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
+    public void testStates_enabledMultiFinger_areMutuallyExclusive() {
+        forEachState(state1 -> {
+            forEachState(state2 -> {
+                if (state1 < state2) {
+                    goFromStateIdleTo(state1);
+                    try {
+                        assertIn(state2);
+                        fail("State " + stateToString(state1) + " also implies state "
+                                + stateToString(state2) + stateDump());
+                    } catch (AssertionError e) {
+                        // expected
+                        returnToNormalFrom(state1);
+                    }
+                }
+            }, LAST_STATE_WITH_MULTI_FINGER);
+        }, LAST_STATE_WITH_MULTI_FINGER);
     }
 
     @Test
@@ -187,8 +236,8 @@
         returnToNormalFrom(STATE_SHOW_MAGNIFIER_TRIPLE_TAP);
     }
 
-    private void forEachState(IntConsumer action) {
-        for (int state = FIRST_STATE; state <= LAST_STATE; state++) {
+    private void forEachState(IntConsumer action, int lastState) {
+        for (int state = FIRST_STATE; state <= lastState; state++) {
             action.accept(state);
         }
     }
@@ -207,14 +256,16 @@
             }
             break;
             case STATE_SHOW_MAGNIFIER_SHORTCUT:
-            case STATE_SHOW_MAGNIFIER_TRIPLE_TAP: {
+            case STATE_SHOW_MAGNIFIER_TRIPLE_TAP:
+            case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP:
                 check(isWindowMagnifierEnabled(DISPLAY_0), state);
                 check(mWindowMagnificationGestureHandler.mCurrentState
                         == mWindowMagnificationGestureHandler.mDetectingState, state);
-            }
                 break;
             case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
-            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD:
+            case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: {
                 check(isWindowMagnifierEnabled(DISPLAY_0), state);
                 check(mWindowMagnificationGestureHandler.mCurrentState
                         == mWindowMagnificationGestureHandler.mViewportDraggingState, state);
@@ -286,6 +337,29 @@
                     tapAndHold();
                 }
                 break;
+                case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: {
+                    twoFingerTap();
+                    twoFingerTap();
+                    twoFingerTap();
+                    // Wait for two-finger tap gesture completed.
+                    SystemClock.sleep(ViewConfiguration.getDoubleTapMinTime() + 500);
+                    InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+                }
+                break;
+                case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: {
+                    twoFingerTap();
+                    twoFingerTap();
+                    twoFingerTapAndHold();
+                }
+                break;
+                case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: {
+                    // enabled then perform two finger triple tap and hold gesture
+                    goFromStateIdleTo(STATE_SHOW_MAGNIFIER_SHORTCUT);
+                    twoFingerTap();
+                    twoFingerTap();
+                    twoFingerTapAndHold();
+                }
+                break;
                 default:
                     throw new IllegalArgumentException("Illegal state: " + state);
             }
@@ -319,13 +393,22 @@
                 tap();
             }
             break;
-            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD:
                 send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
-            }
-            break;
-            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+                break;
+            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD:
                 send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
                 returnToNormalFrom(STATE_SHOW_MAGNIFIER_SHORTCUT);
+                break;
+            case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: {
+                twoFingerTap();
+                twoFingerTap();
+                twoFingerTap();
+                // Wait for two-finger tap gesture completed.
+                SystemClock.sleep(ViewConfiguration.getDoubleTapMinTime() + 500);
+                InstrumentationRegistry.getInstrumentation().waitForIdleSync();
             }
             break;
             default:
@@ -365,6 +448,16 @@
         return TouchEventGenerator.downEvent(DISPLAY_0, x, y);
     }
 
+    private MotionEvent pointerDownEvent(float x, float y) {
+        return TouchEventGenerator.pointerDownEvent(DISPLAY_0,
+                new PointF[] {DEFAULT_POINT, new PointF(x, y)});
+    }
+
+    private MotionEvent pointerUpEvent(float x, float y) {
+        return TouchEventGenerator.pointerUpEvent(DISPLAY_0,
+                new PointF[] {DEFAULT_POINT, new PointF(x, y)});
+    }
+
     private MotionEvent upEvent(float x, float y) {
         return TouchEventGenerator.upEvent(DISPLAY_0, x, y);
     }
@@ -379,6 +472,19 @@
         SystemClock.sleep(ViewConfiguration.getLongPressTimeout() + 100);
     }
 
+    private void twoFingerTap() {
+        send(downEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+        send(pointerDownEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y));
+        send(pointerUpEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y));
+        send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+    }
+
+    private void twoFingerTapAndHold() {
+        send(downEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+        send(pointerDownEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() + 100);
+    }
+
     private String stateDump() {
         return "\nCurrent state dump:\n" + mWindowMagnificationGestureHandler.mCurrentState;
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java
index fbcde53..fcd16a0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java
@@ -39,10 +39,36 @@
         return generateSingleTouchEvent(displayId, ACTION_DOWN, x, y);
     }
 
+    /**
+     * Create a test {@link MotionEvent#ACTION_POINTER_DOWN}, filling in all the basic values that
+     * define the motion.
+     *
+     * @param displayId The id of the display
+     * @param pointFs location on the screen of the all pointers
+     */
+    public static MotionEvent pointerDownEvent(int displayId, PointF[] pointFs) {
+        final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int action = ACTION_POINTER_DOWN | actionIndex;
+        return generateMultiplePointersEvent(displayId, action, pointFs);
+    }
+
     public static MotionEvent moveEvent(int displayId, float x, float y) {
         return generateSingleTouchEvent(displayId, ACTION_MOVE, x, y);
     }
 
+    /**
+     * Create a test {@link MotionEvent#ACTION_POINTER_UP}, filling in all the basic values that
+     * define the motion.
+     *
+     * @param displayId the id of the display
+     * @param pointFs location on the screen of the all pointers
+     */
+    public static MotionEvent pointerUpEvent(int displayId, PointF[] pointFs) {
+        final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int action = ACTION_POINTER_UP | actionIndex;
+        return generateMultiplePointersEvent(displayId, action, pointFs);
+    }
+
     public static MotionEvent upEvent(int displayId, float x, float y) {
         return generateSingleTouchEvent(displayId, ACTION_UP, x, y);
     }
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 4e9ac7c..d4d3128 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.audio;
 
+import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
@@ -28,13 +30,22 @@
 import android.media.AudioSystem;
 import android.media.VolumeInfo;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.InstrumentationRegistry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 public class AudioDeviceVolumeManagerTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String TAG = "AudioDeviceVolumeManagerTest";
 
     private static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes(
@@ -96,4 +107,91 @@
         verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
                 AudioManager.STREAM_MUSIC, midIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
     }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+    public void testConfigurablePreScaleAbsoluteVolume() throws Exception {
+        AudioManager am = mContext.getSystemService(AudioManager.class);
+        final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+        final int maxIndex = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        final VolumeInfo volMedia = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                .setMinVolumeIndex(minIndex)
+                .setMaxVolumeIndex(maxIndex)
+                .build();
+        final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
+                /*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "fake_ble");
+        final int maxPreScaleIndex = 3;
+        final float[] preScale = new float[3];
+        preScale[0] = mContext.getResources().getFraction(
+                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1,
+                1, 1);
+        preScale[1] = mContext.getResources().getFraction(
+                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2,
+                1, 1);
+        preScale[2] = mContext.getResources().getFraction(
+                com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3,
+                1, 1);
+
+        for (int i = 0; i < maxPreScaleIndex; i++) {
+            final int targetIndex = (int) (preScale[i] * maxIndex);
+            final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
+                    .setVolumeIndex(i + 1).build();
+            // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:1~3)
+            mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName);
+            mTestLooper.dispatchAll();
+
+            // Stream volume changes
+            verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
+                            AudioManager.STREAM_MUSIC, targetIndex,
+                            AudioSystem.DEVICE_OUT_BLE_HEADSET);
+        }
+
+        // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:4)
+        final VolumeInfo volIndex4 = new VolumeInfo.Builder(volMedia)
+                .setVolumeIndex(4).build();
+        mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName);
+        mTestLooper.dispatchAll();
+
+        verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
+                        AudioManager.STREAM_MUSIC, maxIndex,
+                        AudioSystem.DEVICE_OUT_BLE_HEADSET);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+    public void testDisablePreScaleAbsoluteVolume() throws Exception {
+        AudioManager am = mContext.getSystemService(AudioManager.class);
+        final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+        final int maxIndex = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        final VolumeInfo volMedia = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                .setMinVolumeIndex(minIndex)
+                .setMaxVolumeIndex(maxIndex)
+                .build();
+        final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
+                /*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
+        final int maxPreScaleIndex = 3;
+
+        for (int i = 0; i < maxPreScaleIndex; i++) {
+            final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
+                    .setVolumeIndex(i + 1).build();
+            // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:1~3)
+            mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName);
+            mTestLooper.dispatchAll();
+
+            // Stream volume changes
+            verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
+                            AudioManager.STREAM_MUSIC, maxIndex,
+                            AudioSystem.DEVICE_OUT_BLE_HEADSET);
+        }
+
+        // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:4)
+        final VolumeInfo volIndex4 = new VolumeInfo.Builder(volMedia)
+                .setVolumeIndex(4).build();
+        mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName);
+        mTestLooper.dispatchAll();
+
+        verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
+                        AudioManager.STREAM_MUSIC, maxIndex,
+                        AudioSystem.DEVICE_OUT_BLE_HEADSET);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
index ad09ef0..061b8ff 100644
--- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
@@ -79,7 +79,7 @@
         final AudioDeviceAttributes dev3 =
                 new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop");
 
-        doNothing().when(mSpyDeviceBroker).persistAudioDeviceSettings();
+        doNothing().when(mSpyDeviceBroker).postPersistAudioDeviceSettings();
         mSpatHelper.initForTest(true /*binaural*/, true /*transaural*/);
 
         // test with single device
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 6d67c8b..5b88c8c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -970,6 +970,13 @@
         assertEquals(
                 "Screen orientation must be defined by the window even on close-to-square display.",
                 window.mAttrs.screenOrientation, dc.getOrientation());
+
+        // Assume that a decor window occupies the display height, so the configuration orientation
+        // should be landscape.
+        dc.getDisplayPolicy().getDecorInsetsInfo(ROTATION_0, dc.mBaseDisplayHeight,
+                dc.mBaseDisplayWidth).mConfigFrame.set(0, 0, 1000, 990);
+        dc.computeScreenConfiguration(config, ROTATION_0);
+        assertEquals(Configuration.ORIENTATION_LANDSCAPE, config.orientation);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 7822071..ec068be 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -27,12 +27,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -130,17 +126,6 @@
     }
 
     @Test
-    public void testUpdateOrganizedTaskFragmentSurface_noSurfaceUpdateWhenOrganizedBySystem() {
-        clearInvocations(mTransaction);
-        mTaskFragment.mIsSurfaceManagedBySystemOrganizer = true;
-
-        mTaskFragment.updateOrganizedTaskFragmentSurface();
-
-        verify(mTransaction, never()).setPosition(eq(mLeash), anyFloat(), anyFloat());
-        verify(mTransaction, never()).setWindowCrop(eq(mLeash), anyInt(), anyInt());
-    }
-
-    @Test
     public void testShouldStartChangeTransition_relativePositionChange() {
         final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW,
                 ACTIVITY_TYPE_STANDARD);
diff --git a/services/usb/Android.bp b/services/usb/Android.bp
index 9f3b52e..1dc5dcf 100644
--- a/services/usb/Android.bp
+++ b/services/usb/Android.bp
@@ -33,6 +33,6 @@
         "android.hardware.usb-V1.1-java",
         "android.hardware.usb-V1.2-java",
         "android.hardware.usb-V1.3-java",
-        "android.hardware.usb-V2-java",
+        "android.hardware.usb-V3-java",
     ],
 }
diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java
index 35e2fcf..fb13b33 100644
--- a/services/usb/java/com/android/server/usb/UsbService.java
+++ b/services/usb/java/com/android/server/usb/UsbService.java
@@ -1285,12 +1285,17 @@
                 pw.println("  dumpsys usb add-port \"matrix\" dual --compliance-warnings");
                 pw.println("  dumpsys usb set-compliance-reasons \"matrix\" <reason-list>");
                 pw.println("  dumpsys usb clear-compliance-reasons \"matrix\"");
-                pw.println("<reason-list> is expected to be formatted as \"1, ..., 4\"");
+                pw.println("<reason-list> is expected to be formatted as \"1, ..., N\"");
                 pw.println("with reasons that need to be simulated.");
                 pw.println("  1: other");
                 pw.println("  2: debug accessory");
                 pw.println("  3: bc12");
                 pw.println("  4: missing rp");
+                pw.println("  5: input power limited");
+                pw.println("  6: missing data lines");
+                pw.println("  7: enumeration fail");
+                pw.println("  8: flaky connection");
+                pw.println("  9: unreliable io");
                 pw.println();
                 pw.println("Example simulate DisplayPort Alt Mode Changes:");
                 pw.println("  dumpsys usb add-port \"matrix\" dual --displayport");
diff --git a/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java b/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
index c7a7a9b..45b623b 100644
--- a/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
+++ b/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
@@ -39,6 +39,7 @@
 import android.hardware.usb.AltModeData;
 import android.hardware.usb.AltModeData.DisplayPortAltModeData;
 import android.hardware.usb.DisplayPortAltModePinAssignment;
+import android.hardware.usb.flags.Flags;
 import android.os.Build;
 import android.os.ServiceManager;
 import android.os.IBinder;
@@ -593,11 +594,21 @@
             for (int warning : complianceWarnings) {
                 if (newComplianceWarnings.indexOf(warning) == -1
                         && warning >= UsbPortStatus.COMPLIANCE_WARNING_OTHER) {
-                    // ComplianceWarnings range from [1, 4] in Android U
-                    if (warning > UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP) {
-                        newComplianceWarnings.add(UsbPortStatus.COMPLIANCE_WARNING_OTHER);
+                    if (Flags.enableUsbDataComplianceWarning()) {
+                        // ComplianceWarnings range extends to [1, 9] when feature flag is on
+                        if (warning
+                                > UsbPortStatus.COMPLIANCE_WARNING_UNRELIABLE_IO) {
+                            newComplianceWarnings.add(UsbPortStatus.COMPLIANCE_WARNING_OTHER);
+                        } else {
+                            newComplianceWarnings.add(warning);
+                        }
                     } else {
-                        newComplianceWarnings.add(warning);
+                        // ComplianceWarnings range from [1, 4] in Android U
+                        if (warning > UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP) {
+                            newComplianceWarnings.add(UsbPortStatus.COMPLIANCE_WARNING_OTHER);
+                        } else {
+                            newComplianceWarnings.add(warning);
+                        }
                     }
                 }
             }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
index 216f45a..d722f2f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
@@ -279,6 +279,7 @@
                 mFullyBound = mContext.bindServiceAsUser(mBindIntent, mFullConnection,
                         Context.BIND_AUTO_CREATE | Context.BIND_TREAT_LIKE_ACTIVITY
                                 | Context.BIND_SCHEDULE_LIKE_TOP_APP
+                                | Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
                                 | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
                         new UserHandle(mUser));
             }
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index cb7926c..11cbcb1 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -568,7 +568,6 @@
     private final int mSkip464Xlat;
     private final boolean mAlwaysOn;
     private final @InfrastructureBitmask int mInfrastructureBitmask;
-    private final boolean mEsimBootstrapProvisioning;
 
     /**
      * Returns the default MTU (Maximum Transmission Unit) size in bytes of the IPv4 routes brought
@@ -980,18 +979,6 @@
         return mInfrastructureBitmask;
     }
 
-    /**
-     * Returns esim bootstrap provisioning flag for which the APN can be used on. For example,
-     * some APNs are only allowed to bring up network, when the device esim bootstrap provisioning
-     * is being activated.
-     *
-     * {@code true} if the APN is used for eSIM bootstrap provisioning, {@code false} otherwise.
-     * @hide
-     */
-    public boolean isEsimBootstrapProvisioning() {
-        return mEsimBootstrapProvisioning;
-    }
-
     private ApnSetting(Builder builder) {
         this.mEntryName = builder.mEntryName;
         this.mApnName = builder.mApnName;
@@ -1029,7 +1016,6 @@
         this.mSkip464Xlat = builder.mSkip464Xlat;
         this.mAlwaysOn = builder.mAlwaysOn;
         this.mInfrastructureBitmask = builder.mInfrastructureBitmask;
-        this.mEsimBootstrapProvisioning = builder.mEsimBootstrapProvisioning;
     }
 
     /**
@@ -1111,8 +1097,6 @@
                 .setAlwaysOn(cursor.getInt(cursor.getColumnIndexOrThrow(Carriers.ALWAYS_ON)) == 1)
                 .setInfrastructureBitmask(cursor.getInt(cursor.getColumnIndexOrThrow(
                         Telephony.Carriers.INFRASTRUCTURE_BITMASK)))
-                .setEsimBootstrapProvisioning(cursor.getInt(
-                        cursor.getColumnIndexOrThrow(Carriers.ESIM_BOOTSTRAP_PROVISIONING)) == 1)
                 .buildWithoutCheck();
     }
 
@@ -1153,7 +1137,6 @@
                 .setSkip464Xlat(apn.mSkip464Xlat)
                 .setAlwaysOn(apn.mAlwaysOn)
                 .setInfrastructureBitmask(apn.mInfrastructureBitmask)
-                .setEsimBootstrapProvisioning(apn.mEsimBootstrapProvisioning)
                 .buildWithoutCheck();
     }
 
@@ -1201,7 +1184,6 @@
         sb.append(", ").append(mAlwaysOn);
         sb.append(", ").append(mInfrastructureBitmask);
         sb.append(", ").append(Objects.hash(mUser, mPassword));
-        sb.append(", ").append(mEsimBootstrapProvisioning);
         return sb.toString();
     }
 
@@ -1265,7 +1247,7 @@
                 mProtocol, mRoamingProtocol, mMtuV4, mMtuV6, mCarrierEnabled, mNetworkTypeBitmask,
                 mLingeringNetworkTypeBitmask, mProfileId, mPersistent, mMaxConns, mWaitTime,
                 mMaxConnsTime, mMvnoType, mMvnoMatchData, mApnSetId, mCarrierId, mSkip464Xlat,
-                mAlwaysOn, mInfrastructureBitmask, mEsimBootstrapProvisioning);
+                mAlwaysOn, mInfrastructureBitmask);
     }
 
     @Override
@@ -1307,8 +1289,7 @@
                 && mCarrierId == other.mCarrierId
                 && mSkip464Xlat == other.mSkip464Xlat
                 && mAlwaysOn == other.mAlwaysOn
-                && mInfrastructureBitmask == other.mInfrastructureBitmask
-                && Objects.equals(mEsimBootstrapProvisioning, other.mEsimBootstrapProvisioning);
+                && mInfrastructureBitmask == other.mInfrastructureBitmask;
     }
 
     /**
@@ -1359,8 +1340,7 @@
                 && Objects.equals(mCarrierId, other.mCarrierId)
                 && Objects.equals(mSkip464Xlat, other.mSkip464Xlat)
                 && Objects.equals(mAlwaysOn, other.mAlwaysOn)
-                && Objects.equals(mInfrastructureBitmask, other.mInfrastructureBitmask)
-                && Objects.equals(mEsimBootstrapProvisioning, other.mEsimBootstrapProvisioning);
+                && Objects.equals(mInfrastructureBitmask, other.mInfrastructureBitmask);
     }
 
     /**
@@ -1398,9 +1378,7 @@
                 && Objects.equals(this.mCarrierId, other.mCarrierId)
                 && Objects.equals(this.mSkip464Xlat, other.mSkip464Xlat)
                 && Objects.equals(this.mAlwaysOn, other.mAlwaysOn)
-                && Objects.equals(this.mInfrastructureBitmask, other.mInfrastructureBitmask)
-                && Objects.equals(this.mEsimBootstrapProvisioning,
-                other.mEsimBootstrapProvisioning);
+                && Objects.equals(this.mInfrastructureBitmask, other.mInfrastructureBitmask);
     }
 
     // Equal or one is null.
@@ -1473,7 +1451,6 @@
         apnValue.put(Telephony.Carriers.SKIP_464XLAT, mSkip464Xlat);
         apnValue.put(Telephony.Carriers.ALWAYS_ON, mAlwaysOn);
         apnValue.put(Telephony.Carriers.INFRASTRUCTURE_BITMASK, mInfrastructureBitmask);
-        apnValue.put(Carriers.ESIM_BOOTSTRAP_PROVISIONING, mEsimBootstrapProvisioning);
         return apnValue;
     }
 
@@ -1747,7 +1724,6 @@
         dest.writeInt(mSkip464Xlat);
         dest.writeBoolean(mAlwaysOn);
         dest.writeInt(mInfrastructureBitmask);
-        dest.writeBoolean(mEsimBootstrapProvisioning);
     }
 
     private static ApnSetting readFromParcel(Parcel in) {
@@ -1784,7 +1760,6 @@
                 .setSkip464Xlat(in.readInt())
                 .setAlwaysOn(in.readBoolean())
                 .setInfrastructureBitmask(in.readInt())
-                .setEsimBootstrapProvisioning(in.readBoolean())
                 .buildWithoutCheck();
     }
 
@@ -1867,7 +1842,6 @@
         private int mSkip464Xlat = Carriers.SKIP_464XLAT_DEFAULT;
         private boolean mAlwaysOn;
         private int mInfrastructureBitmask = INFRASTRUCTURE_CELLULAR;
-        private boolean mEsimBootstrapProvisioning;
 
         /**
          * Default constructor for Builder.
@@ -2306,19 +2280,6 @@
         }
 
         /**
-         * Sets esim bootstrap provisioning flag
-         *
-         * @param esimBootstrapProvisioning {@code true} if the APN is used for eSIM bootstrap
-         * provisioning, {@code false} otherwise.
-         * @hide
-         */
-        @NonNull
-        public Builder setEsimBootstrapProvisioning(boolean esimBootstrapProvisioning) {
-            this.mEsimBootstrapProvisioning = esimBootstrapProvisioning;
-            return this;
-        }
-
-        /**
          * Builds {@link ApnSetting} from this builder.
          *
          * @return {@code null} if {@link #setApnName(String)} or {@link #setEntryName(String)}
diff --git a/tests/FlickerTests/ActivityEmbedding/Android.bp b/tests/FlickerTests/ActivityEmbedding/Android.bp
new file mode 100644
index 0000000..9eeec7c
--- /dev/null
+++ b/tests/FlickerTests/ActivityEmbedding/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FlickerTestsOther",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    package_name: "com.android.server.wm.flicker",
+    instrumentation_target_package: "com.android.server.wm.flicker",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml
similarity index 91%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml
index 6bc7cbe..f867ffb 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flick">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
index ed71531..439cf13 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
@@ -84,20 +84,6 @@
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/ActivityEmbedding/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/ActivityEmbedding/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/ActivityEmbedding/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/ActivityEmbedding/res/xml/network_security_config.xml
similarity index 63%
rename from tests/FlickerTests/manifests/AndroidManifestOther.xml
rename to tests/FlickerTests/ActivityEmbedding/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/ActivityEmbedding/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
similarity index 99%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
index adff579..955e801 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
@@ -23,9 +23,9 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.FlakyTest
+import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
-import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
similarity index 93%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
index 47d6d23..790da34 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
@@ -36,9 +36,8 @@
 /**
  * Test launching a secondary Activity into Picture-In-Picture mode.
  *
- * Setup: Start from a split A|B.
- * Transition: B enters PIP, observe the window first goes fullscreen then shrink to the bottom
- * right corner on screen.
+ * Setup: Start from a split A|B. Transition: B enters PIP, observe the window first goes fullscreen
+ * then shrink to the bottom right corner on screen.
  *
  * To run this test: `atest FlickerTestsOther:SecondaryActivityEnterPipTest`
  */
@@ -64,16 +63,10 @@
         }
     }
 
-    /**
-     * We expect the background layer to be visible during this transition.
-     */
-    @Presubmit
-    @Test
-    override fun backgroundLayerNeverVisible(): Unit {}
+    /** We expect the background layer to be visible during this transition. */
+    @Presubmit @Test override fun backgroundLayerNeverVisible() {}
 
-    /**
-     * Main and secondary activity start from a split each taking half of the screen.
-     */
+    /** Main and secondary activity start from a split each taking half of the screen. */
     @Presubmit
     @Test
     fun layersStartFromEqualSplit() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
similarity index 97%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
index 4f7d8a4..e8389d19 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
@@ -23,9 +23,7 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
-import com.android.server.wm.flicker.rotation.RotationTransition
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt
new file mode 100644
index 0000000..1123c5b
--- /dev/null
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding.rotation
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import com.android.server.wm.flicker.BaseTest
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.Test
+
+/** Base class for app rotation tests */
+abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) {
+    protected abstract val testApp: StandardAppHelper
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup { this.setRotation(flicker.scenario.startRotation) }
+        teardown { testApp.exit(wmHelper) }
+        transitions { this.setRotation(flicker.scenario.endRotation) }
+    }
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                ignoreLayers =
+                    listOf(
+                        ComponentNameMatcher.SPLASH_SCREEN,
+                        ComponentNameMatcher.SNAPSHOT,
+                        ComponentNameMatcher("", "SecondaryHomeHandle")
+                    )
+            )
+        }
+    }
+
+    /** Checks that [testApp] layer covers the entire screen at the start of the transition */
+    @Presubmit
+    @Test
+    open fun appLayerRotates_StartingPos() {
+        flicker.assertLayersStart {
+            this.entry.displays.map { display ->
+                this.visibleRegion(testApp).coversExactly(display.layerStackSpace)
+            }
+        }
+    }
+
+    /** Checks that [testApp] layer covers the entire screen at the end of the transition */
+    @Presubmit
+    @Test
+    open fun appLayerRotates_EndingPos() {
+        flicker.assertLayersEnd {
+            this.entry.displays.map { display ->
+                this.visibleRegion(testApp).coversExactly(display.layerStackSpace)
+            }
+        }
+    }
+
+    override fun cujCompleted() {
+        super.cujCompleted()
+        appLayerRotates_StartingPos()
+        appLayerRotates_EndingPos()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
similarity index 63%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
rename to tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
index 93a5bf5..576eec8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
@@ -24,8 +24,8 @@
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.traces.parsers.toFlickerComponent
-import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
 import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.utils.SplitScreenUtils
@@ -39,11 +39,11 @@
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
 
-/***
+/**
  * Test entering System SplitScreen with Activity Embedding Split and another app.
  *
- * Setup: Launch A|B in split and secondaryApp, return to home.
- * Transitions: Let AE Split A|B enter splitscreen with secondaryApp. Resulting in A|B|secondaryApp.
+ * Setup: Launch A|B in split and secondaryApp, return to home. Transitions: Let AE Split A|B enter
+ * splitscreen with secondaryApp. Resulting in A|B|secondaryApp.
  *
  * To run this test: `atest FlickerTestsOther:EnterSystemSplitTest`
  */
@@ -51,8 +51,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSystemSplitTest(flicker: LegacyFlickerTest) :
-        ActivityEmbeddingTestBase(flicker) {
+class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBase(flicker) {
 
     private val secondaryApp = SplitScreenUtils.getPrimary(instrumentation)
     override val transition: FlickerBuilder.() -> Unit = {
@@ -62,17 +61,22 @@
             secondaryApp.launchViaIntent(wmHelper)
             tapl.goHome()
             wmHelper
-                    .StateSyncBuilder()
-                    .withAppTransitionIdle()
-                    .withHomeActivityVisible()
-                    .waitForAndVerify()
+                .StateSyncBuilder()
+                .withAppTransitionIdle()
+                .withHomeActivityVisible()
+                .waitForAndVerify()
             startDisplayBounds =
-                    wmHelper.currentState.layerState.physicalDisplayBounds
-                        ?: error("Display not found")
+                wmHelper.currentState.layerState.physicalDisplayBounds ?: error("Display not found")
         }
         transitions {
-            SplitScreenUtils.enterSplit(wmHelper, tapl, device, testApp, secondaryApp,
-                flicker.scenario.startRotation)
+            SplitScreenUtils.enterSplit(
+                wmHelper,
+                tapl,
+                device,
+                testApp,
+                secondaryApp,
+                flicker.scenario.startRotation
+            )
             SplitScreenUtils.waitForSplitComplete(wmHelper, testApp, secondaryApp)
         }
     }
@@ -85,7 +89,10 @@
     @Test
     fun activityEmbeddingSplitLayerBecomesVisible() {
         flicker.splitAppLayerBoundsIsVisibleAtEnd(
-                testApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
+            testApp,
+            landscapePosLeft = tapl.isTablet,
+            portraitPosTop = false
+        )
     }
 
     @Presubmit
@@ -96,7 +103,10 @@
     @Test
     fun secondaryLayerBecomesVisible() {
         flicker.splitAppLayerBoundsIsVisibleAtEnd(
-                secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true)
+            secondaryApp,
+            landscapePosLeft = !tapl.isTablet,
+            portraitPosTop = true
+        )
     }
 
     @Presubmit
@@ -105,73 +115,73 @@
 
     /**
      * After the transition there should be both ActivityEmbedding activities,
-     * SplitScreenPrimaryActivity and the system split divider on screen.
-     * Verify the layers are in expected sizes.
+     * SplitScreenPrimaryActivity and the system split divider on screen. Verify the layers are in
+     * expected sizes.
      */
     @Presubmit
     @Test
     fun activityEmbeddingSplitSurfaceAreEven() {
         flicker.assertLayersEnd {
             val leftAELayerRegion =
-                    visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
             val rightAELayerRegion =
-                    visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
             val secondaryAppLayerRegion =
-                    visibleRegion(
-                            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent())
+                visibleRegion(ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent())
             val systemDivider = visibleRegion(SPLIT_SCREEN_DIVIDER_COMPONENT)
             leftAELayerRegion
-                    .plus(rightAELayerRegion.region)
-                    .plus(secondaryAppLayerRegion.region)
-                    .plus(systemDivider.region)
-                    .coversExactly(startDisplayBounds)
+                .plus(rightAELayerRegion.region)
+                .plus(secondaryAppLayerRegion.region)
+                .plus(systemDivider.region)
+                .coversExactly(startDisplayBounds)
             check { "ActivityEmbeddingSplitHeight" }
-                    .that(leftAELayerRegion.region.height)
-                    .isEqual(rightAELayerRegion.region.height)
+                .that(leftAELayerRegion.region.height)
+                .isEqual(rightAELayerRegion.region.height)
             check { "SystemSplitHeight" }
-                    .that(rightAELayerRegion.region.height)
-                    .isEqual(secondaryAppLayerRegion.region.height)
+                .that(rightAELayerRegion.region.height)
+                .isEqual(secondaryAppLayerRegion.region.height)
             // TODO(b/292283182): Remove this special case handling.
             check { "ActivityEmbeddingSplitWidth" }
-                    .that(Math.abs(
-                            leftAELayerRegion.region.width - rightAELayerRegion.region.width))
-                    .isLower(2)
+                .that(Math.abs(leftAELayerRegion.region.width - rightAELayerRegion.region.width))
+                .isLower(2)
             check { "SystemSplitWidth" }
-                    .that(Math.abs(secondaryAppLayerRegion.region.width -
-                            2 * rightAELayerRegion.region.width))
-                    .isLower(2)
+                .that(
+                    Math.abs(
+                        secondaryAppLayerRegion.region.width - 2 * rightAELayerRegion.region.width
+                    )
+                )
+                .isLower(2)
         }
     }
 
-    /**
-     * Verify the windows are in expected sizes.
-     */
+    /** Verify the windows are in expected sizes. */
     @Presubmit
     @Test
     fun activityEmbeddingSplitWindowsAreEven() {
         flicker.assertWmEnd {
             val leftAEWindowRegion =
-                    visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
             val rightAEWindowRegion =
-                    visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
             // There's no window for the divider bar.
             val secondaryAppLayerRegion =
-                    visibleRegion(
-                            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent())
+                visibleRegion(ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent())
             check { "ActivityEmbeddingSplitHeight" }
-                    .that(leftAEWindowRegion.region.height)
-                    .isEqual(rightAEWindowRegion.region.height)
+                .that(leftAEWindowRegion.region.height)
+                .isEqual(rightAEWindowRegion.region.height)
             check { "SystemSplitHeight" }
-                    .that(rightAEWindowRegion.region.height)
-                    .isEqual(secondaryAppLayerRegion.region.height)
+                .that(rightAEWindowRegion.region.height)
+                .isEqual(secondaryAppLayerRegion.region.height)
             check { "ActivityEmbeddingSplitWidth" }
-                    .that(Math.abs(
-                            leftAEWindowRegion.region.width - rightAEWindowRegion.region.width))
-                    .isLower(2)
+                .that(Math.abs(leftAEWindowRegion.region.width - rightAEWindowRegion.region.width))
+                .isLower(2)
             check { "SystemSplitWidth" }
-                    .that(Math.abs(secondaryAppLayerRegion.region.width -
-                            2 * rightAEWindowRegion.region.width))
-                    .isLower(2)
+                .that(
+                    Math.abs(
+                        secondaryAppLayerRegion.region.width - 2 * rightAEWindowRegion.region.width
+                    )
+                )
+                .isLower(2)
         }
     }
 
@@ -191,4 +201,4 @@
         @JvmStatic
         fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
-}
\ No newline at end of file
+}
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/ActivityEmbedding/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/ActivityEmbedding/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 3d49d81..514f895 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -24,75 +24,14 @@
 }
 
 filegroup {
-    name: "FlickerTestsBase-src",
-    srcs: ["src/com/android/server/wm/flicker/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsAppClose-src",
-    srcs: ["src/**/close/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsActivityEmbedding-src",
-    srcs: [
-        "src/**/activityembedding/*.kt",
-        "src/**/activityembedding/open/*.kt",
-        "src/**/activityembedding/close/*.kt",
-        "src/**/activityembedding/layoutchange/*.kt",
-        "src/**/activityembedding/pip/*.kt",
-        "src/**/activityembedding/rotation/*.kt",
-        "src/**/activityembedding/rtl/*.kt",
-        "src/**/activityembedding/splitscreen/*.kt",
-    ],
-}
-
-filegroup {
-    name: "FlickerTestsIme-src",
-    srcs: ["src/**/ime/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsAppLaunchCommon-src",
-    srcs: ["src/**/launch/common/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsAppLaunch1-src",
-    srcs: ["src/**/launch/OpenApp*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsAppLaunch2-src",
-    srcs: ["src/**/launch/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsNotification-src",
-    srcs: ["src/**/notification/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsQuickswitch-src",
-    srcs: ["src/**/quickswitch/*.kt"],
-}
-
-filegroup {
-    name: "FlickerTestsRotation-src",
-    srcs: ["src/**/rotation/*.kt"],
-}
-
-filegroup {
     name: "FlickerServiceTests-src",
     srcs: [
-        "src/com/android/server/wm/flicker/service/**/*.kt",
+        "src/**/*",
     ],
 }
 
 java_defaults {
     name: "FlickerTestsDefault",
-    manifest: "manifests/AndroidManifest.xml",
-    test_config_template: "AndroidTestTemplate.xml",
     platform_apis: true,
     certificate: "platform",
     optimize: {
@@ -116,139 +55,6 @@
     ],
 }
 
-android_test {
-    name: "FlickerTestsOther",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestOther.xml"],
-    package_name: "com.android.server.wm.flicker",
-    instrumentation_target_package: "com.android.server.wm.flicker",
-    srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
-    ],
-    exclude_srcs: [
-        ":FlickerTestsAppClose-src",
-        ":FlickerTestsIme-src",
-        ":FlickerTestsAppLaunch1-src",
-        ":FlickerTestsAppLaunch2-src",
-        ":FlickerTestsQuickswitch-src",
-        ":FlickerTestsRotation-src",
-        ":FlickerTestsNotification-src",
-        ":FlickerServiceTests-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsAppClose",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestAppClose.xml"],
-    package_name: "com.android.server.wm.flicker.close",
-    instrumentation_target_package: "com.android.server.wm.flicker.close",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsAppClose-src",
-    ],
-    exclude_srcs: [
-        ":FlickerTestsActivityEmbedding-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsIme",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestIme.xml"],
-    package_name: "com.android.server.wm.flicker.ime",
-    instrumentation_target_package: "com.android.server.wm.flicker.ime",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsIme-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsAppLaunch1",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestAppLaunch.xml"],
-    package_name: "com.android.server.wm.flicker.launch",
-    instrumentation_target_package: "com.android.server.wm.flicker.launch",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsAppLaunchCommon-src",
-        ":FlickerTestsAppLaunch1-src",
-    ],
-    exclude_srcs: [
-        ":FlickerTestsActivityEmbedding-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsAppLaunch2",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestAppLaunch.xml"],
-    package_name: "com.android.server.wm.flicker.launch",
-    instrumentation_target_package: "com.android.server.wm.flicker.launch",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsAppLaunchCommon-src",
-        ":FlickerTestsAppLaunch2-src",
-    ],
-    exclude_srcs: [
-        ":FlickerTestsActivityEmbedding-src",
-        ":FlickerTestsAppLaunch1-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsNotification",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestNotification.xml"],
-    package_name: "com.android.server.wm.flicker.notification",
-    instrumentation_target_package: "com.android.server.wm.flicker.notification",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsNotification-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsQuickswitch",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestQuickswitch.xml"],
-    package_name: "com.android.server.wm.flicker.quickswitch",
-    instrumentation_target_package: "com.android.server.wm.flicker.quickswitch",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsQuickswitch-src",
-    ],
-}
-
-android_test {
-    name: "FlickerTestsRotation",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestRotation.xml"],
-    package_name: "com.android.server.wm.flicker.rotation",
-    instrumentation_target_package: "com.android.server.wm.flicker.rotation",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerTestsRotation-src",
-    ],
-    exclude_srcs: [
-        ":FlickerTestsActivityEmbedding-src",
-    ],
-}
-
-android_test {
-    name: "FlickerServiceTests",
-    defaults: ["FlickerTestsDefault"],
-    additional_manifests: ["manifests/AndroidManifestService.xml"],
-    package_name: "com.android.server.wm.flicker.service",
-    instrumentation_target_package: "com.android.server.wm.flicker.service",
-    srcs: [
-        ":FlickerTestsBase-src",
-        ":FlickerServiceTests-src",
-    ],
-}
-
 java_library {
     name: "wm-flicker-common-assertions",
     platform_apis: true,
@@ -259,9 +65,6 @@
         "src/**/*Assertions.java",
         "src/**/*Assertions.kt",
     ],
-    exclude_srcs: [
-        "**/helpers/*",
-    ],
     static_libs: [
         "flickerlib",
         "flickerlib-helpers",
@@ -270,26 +73,6 @@
     ],
 }
 
-java_library {
-    name: "wm-flicker-common-app-helpers",
-    platform_apis: true,
-    optimize: {
-        enabled: false,
-    },
-    srcs: [
-        "**/helpers/*",
-    ],
-    static_libs: [
-        "flickertestapplib",
-        "flickerlib",
-        "flickerlib-apphelpers",
-        "flickerlib-helpers",
-        "truth",
-        "app-helpers-core",
-        "wm-flicker-window-extensions",
-    ],
-}
-
 android_library_import {
     name: "wm-flicker-window-extensions_nodeps",
     aars: ["libs/window-extensions-release.aar"],
@@ -304,3 +87,9 @@
     ],
     installable: false,
 }
+
+java_library {
+    name: "FlickerTestsBase",
+    defaults: ["FlickerTestsDefault"],
+    srcs: ["src/**/*"],
+}
diff --git a/tests/FlickerTests/AppClose/Android.bp b/tests/FlickerTests/AppClose/Android.bp
new file mode 100644
index 0000000..151d12f
--- /dev/null
+++ b/tests/FlickerTests/AppClose/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FlickerTestsAppClose",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/AppClose/AndroidManifest.xml
similarity index 91%
rename from tests/FlickerTests/manifests/AndroidManifest.xml
rename to tests/FlickerTests/AppClose/AndroidManifest.xml
index 6bc7cbe..e75e178 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/AppClose/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.close">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.close"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/AppClose/AndroidTestTemplate.xml
index ed71531..4b6224e 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
@@ -83,21 +83,7 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/OWNERS b/tests/FlickerTests/AppClose/OWNERS
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/close/OWNERS
rename to tests/FlickerTests/AppClose/OWNERS
diff --git a/tests/FlickerTests/AppClose/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/AppClose/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/AppClose/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/AppClose/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/AppClose/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/AppClose/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
rename to tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
index 288558ae..64dd44d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
+++ b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
rename to tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
index 32305c6..eb256b5 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
+++ b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
rename to tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
index 8d752cc..ea025c7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
+++ b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/AppClose/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/AppClose/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/AppLaunch/Android.bp b/tests/FlickerTests/AppLaunch/Android.bp
new file mode 100644
index 0000000..f33384d
--- /dev/null
+++ b/tests/FlickerTests/AppLaunch/Android.bp
@@ -0,0 +1,69 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "FlickerTestsAppLaunchCommon-src",
+    srcs: ["src/**/common/*"],
+}
+
+filegroup {
+    name: "FlickerTestsAppLaunch1-src",
+    srcs: ["src/**/OpenApp*"],
+}
+
+java_library {
+    name: "FlickerTestsAppLaunchCommon",
+    defaults: ["FlickerTestsDefault"],
+    srcs: [":FlickerTestsAppLaunchCommon-src"],
+    static_libs: ["FlickerTestsBase"],
+}
+
+android_test {
+    name: "FlickerTestsAppLaunch1",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: [":FlickerTestsAppLaunch1-src"],
+    static_libs: [
+        "FlickerTestsBase",
+        "FlickerTestsAppLaunchCommon",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsAppLaunch2",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    exclude_srcs: [
+        ":FlickerTestsAppLaunchCommon-src",
+        ":FlickerTestsAppLaunch1-src",
+    ],
+    static_libs: [
+        "FlickerTestsBase",
+        "FlickerTestsAppLaunchCommon",
+    ],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/AppLaunch/AndroidManifest.xml
similarity index 91%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/AppLaunch/AndroidManifest.xml
index 6bc7cbe..b89af1a 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/AppLaunch/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.launch">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.launch"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
index ed71531..583bcb7 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
@@ -83,21 +83,7 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OWNERS b/tests/FlickerTests/AppLaunch/OWNERS
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OWNERS
rename to tests/FlickerTests/AppLaunch/OWNERS
diff --git a/tests/FlickerTests/AppLaunch/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/AppLaunch/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/AppLaunch/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/AppLaunch/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/AppLaunch/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/AppLaunch/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
index f788efa..413767c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
similarity index 97%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
index d86dc50..4168bdc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
index be07053..9c55c98 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
index f66eff9..fc6cdb1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
similarity index 99%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
index 65214764..de666dd 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
index 4d31c28..f8a9961 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
index 42e34b3..0aceb35 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenTransferSplashscreenAppFromLauncherTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt
similarity index 97%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenTransferSplashscreenAppFromLauncherTransition.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt
index 97ba99e..f41a2a2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenTransferSplashscreenAppFromLauncherTransition.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker.launch.common
+package com.android.server.wm.flicker.launch
 
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
@@ -24,6 +24,7 @@
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.TransferSplashscreenAppHelper
+import com.android.server.wm.flicker.launch.common.OpenAppFromIconTransition
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
index 98e3646..93ca41c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
@@ -31,7 +31,6 @@
 import android.tools.device.helpers.wakeUpAndGoToHomeScreen
 import androidx.test.filters.RequiresDevice
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.flicker.R
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
 import org.junit.FixMethodOrder
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
similarity index 99%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
index b82a129..9c2899ac 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt
similarity index 99%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt
index c854701..802c755 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt
+++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt
@@ -44,4 +44,4 @@
             }
             teardown { testApp.exit(wmHelper) }
         }
-}
\ No newline at end of file
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromLauncherTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLauncherTransition.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromLauncherTransition.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLauncherTransition.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppTransition.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/common/OpenAppTransition.kt
rename to tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppTransition.kt
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/AppLaunch/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/AppLaunch/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/FlickerService/Android.bp b/tests/FlickerTests/FlickerService/Android.bp
new file mode 100644
index 0000000..1a38115
--- /dev/null
+++ b/tests/FlickerTests/FlickerService/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FlickerServiceTests",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/FlickerService/AndroidManifest.xml
similarity index 91%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/FlickerService/AndroidManifest.xml
index 6bc7cbe..f31e820 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/FlickerService/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.service">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.service"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
index ed71531..d6ae2b3 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
@@ -83,20 +83,6 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
diff --git a/tests/FlickerTests/FlickerService/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/FlickerService/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/FlickerService/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/FlickerService/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/FlickerService/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/FlickerService/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/Utils.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/Utils.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppHomeButton3ButtonPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppSwipeToHomeGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppBackButton.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppBackButton.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppBackButton.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppBackButton.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppHomeButton.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppHomeButton.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppHomeButton.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppHomeButton.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppSwipeToHome.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppSwipeToHome.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppSwipeToHome.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/scenarios/CloseAppSwipeToHome.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationCold3ButtonNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationColdGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarm3ButtonNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWarmGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayApp3ButtonNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromLockscreenNotificationWithOverlayAppGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationCold3ButtonNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationColdGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarm3ButtonNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/flicker/OpenAppFromNotificationWarmGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/NotificationUtils.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/NotificationUtils.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/NotificationUtils.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/NotificationUtils.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationCold.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationCold.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationCold.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationCold.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWarm.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWarm.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWarm.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWarm.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWithOverlayApp.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWithOverlayApp.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWithOverlayApp.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromLockscreenNotificationWithOverlayApp.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationCold.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationCold.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationCold.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationCold.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationWarm.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationWarm.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/notification/scenarios/OpenAppFromNotificationWarm.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsBackGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchBetweenTwoAppsForwardGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavLandscape.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavLandscape.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavLandscape.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavPortrait.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavPortrait.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/flicker/QuickSwitchFromLauncherGesturalNavPortrait.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsForward.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsForward.kt
similarity index 99%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsForward.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsForward.kt
index 3cae1c4..fcf442a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsForward.kt
+++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsForward.kt
@@ -19,6 +19,7 @@
 import android.app.Instrumentation
 import android.tools.common.NavBar
 import android.tools.common.Rotation
+import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
 import android.tools.device.traces.parsers.WindowManagerStateHelper
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
@@ -30,7 +31,6 @@
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
-import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
 
 @Ignore("Base Test Class")
 abstract class QuickSwitchBetweenTwoAppsForward(val rotation: Rotation = Rotation.ROTATION_0) {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchFromLauncher.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchFromLauncher.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchFromLauncher.kt
rename to tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchFromLauncher.kt
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/FlickerService/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/FlickerService/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp
new file mode 100644
index 0000000..057d9fc
--- /dev/null
+++ b/tests/FlickerTests/IME/Android.bp
@@ -0,0 +1,78 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "FlickerTestsImeCommon-src",
+    srcs: ["src/**/common/*"],
+}
+
+filegroup {
+    name: "FlickerTestsIme1-src",
+    srcs: ["src/**/Close*"],
+}
+
+android_test {
+    name: "FlickerTestsIme",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
+
+java_library {
+    name: "FlickerTestsImeCommon",
+    defaults: ["FlickerTestsDefault"],
+    srcs: [":FlickerTestsImeCommon-src"],
+    static_libs: ["FlickerTestsBase"],
+}
+
+android_test {
+    name: "FlickerTestsIme1",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: [":FlickerTestsIme1-src"],
+    static_libs: [
+        "FlickerTestsBase",
+        "FlickerTestsImeCommon",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsIme2",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    exclude_srcs: [
+        ":FlickerTestsIme1-src",
+        ":FlickerTestsImeCommon-src",
+    ],
+    static_libs: [
+        "FlickerTestsBase",
+        "FlickerTestsImeCommon",
+    ],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/IME/AndroidManifest.xml
similarity index 91%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/IME/AndroidManifest.xml
index 6bc7cbe..d6ca683 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/IME/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.ime">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.ime"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/IME/AndroidTestTemplate.xml
index ed71531..988f76f 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -83,21 +83,7 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OWNERS b/tests/FlickerTests/IME/OWNERS
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OWNERS
rename to tests/FlickerTests/IME/OWNERS
diff --git a/tests/FlickerTests/IME/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/IME/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/IME/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/IME/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/IME/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/IME/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/common/CommonAssertions.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
rename to tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/common/CommonAssertions.kt
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/IME/trace_config/trace_config.textproto
similarity index 100%
rename from tests/FlickerTests/trace_config/trace_config.textproto
rename to tests/FlickerTests/IME/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/Notification/Android.bp b/tests/FlickerTests/Notification/Android.bp
new file mode 100644
index 0000000..5bed568
--- /dev/null
+++ b/tests/FlickerTests/Notification/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FlickerTestsNotification",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/Notification/AndroidManifest.xml
similarity index 90%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/Notification/AndroidManifest.xml
index 6bc7cbe..d212c10 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/Notification/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.notification">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.notification"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/Notification/AndroidTestTemplate.xml
index ed71531..4036858 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
@@ -83,21 +83,7 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OWNERS b/tests/FlickerTests/Notification/OWNERS
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OWNERS
rename to tests/FlickerTests/Notification/OWNERS
diff --git a/tests/FlickerTests/Notification/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/Notification/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/Notification/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/Notification/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/Notification/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/Notification/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/Consts.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/Consts.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt
rename to tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/Notification/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/Notification/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/QuickSwitch/Android.bp b/tests/FlickerTests/QuickSwitch/Android.bp
new file mode 100644
index 0000000..64f7183
--- /dev/null
+++ b/tests/FlickerTests/QuickSwitch/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FlickerTestsQuickswitch",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/QuickSwitch/AndroidManifest.xml
similarity index 90%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/QuickSwitch/AndroidManifest.xml
index 6bc7cbe..41b0cd4 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/QuickSwitch/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.quickswitch">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.quickswitch"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
similarity index 86%
copy from tests/FlickerTests/AndroidTestTemplate.xml
copy to tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
index ed71531..797ca4e 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
@@ -83,21 +83,7 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/OWNERS b/tests/FlickerTests/QuickSwitch/OWNERS
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/OWNERS
rename to tests/FlickerTests/QuickSwitch/OWNERS
diff --git a/tests/FlickerTests/QuickSwitch/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/QuickSwitch/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/QuickSwitch/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/QuickSwitch/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/QuickSwitch/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/QuickSwitch/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
rename to tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
rename to tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
rename to tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/QuickSwitch/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/QuickSwitch/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/Rotation/Android.bp b/tests/FlickerTests/Rotation/Android.bp
new file mode 100644
index 0000000..8e93b5b
--- /dev/null
+++ b/tests/FlickerTests/Rotation/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FlickerTestsRotation",
+    defaults: ["FlickerTestsDefault"],
+    manifest: "AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
+    srcs: ["src/**/*"],
+    static_libs: ["FlickerTestsBase"],
+}
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/Rotation/AndroidManifest.xml
similarity index 91%
copy from tests/FlickerTests/manifests/AndroidManifest.xml
copy to tests/FlickerTests/Rotation/AndroidManifest.xml
index 6bc7cbe..6bbb1f6 100644
--- a/tests/FlickerTests/manifests/AndroidManifest.xml
+++ b/tests/FlickerTests/Rotation/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.server.wm.flicker">
+          package="com.android.server.wm.flicker.rotation">
 
     <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
@@ -59,4 +59,9 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
     </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.rotation"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
 </manifest>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
similarity index 86%
rename from tests/FlickerTests/AndroidTestTemplate.xml
rename to tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index ed71531..b5ea739 100644
--- a/tests/FlickerTests/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -83,21 +83,7 @@
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
-        <option name="directory-keys"
                 value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
-        <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/OWNERS b/tests/FlickerTests/Rotation/OWNERS
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/rotation/OWNERS
rename to tests/FlickerTests/Rotation/OWNERS
diff --git a/tests/FlickerTests/Rotation/res/anim/show_hide_show_3000ms.xml b/tests/FlickerTests/Rotation/res/anim/show_hide_show_3000ms.xml
new file mode 100644
index 0000000..7b3f07e
--- /dev/null
+++ b/tests/FlickerTests/Rotation/res/anim/show_hide_show_3000ms.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:duration="1000" />
+
+    <alpha
+        android:startOffset="2000"
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:duration="1000" />
+</set>
\ No newline at end of file
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/Rotation/res/xml/network_security_config.xml
similarity index 63%
copy from tests/FlickerTests/manifests/AndroidManifestOther.xml
copy to tests/FlickerTests/Rotation/res/xml/network_security_config.xml
index 47749b8..4bd9ca0 100644
--- a/tests/FlickerTests/manifests/AndroidManifestOther.xml
+++ b/tests/FlickerTests/Rotation/res/xml/network_security_config.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2023 The Android Open Source Project
   ~
@@ -14,11 +15,8 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
rename to tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
rename to tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
rename to tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/Rotation/trace_config/trace_config.textproto
similarity index 100%
copy from tests/FlickerTests/trace_config/trace_config.textproto
copy to tests/FlickerTests/Rotation/trace_config/trace_config.textproto
diff --git a/tests/FlickerTests/manifests/AndroidManifestAppClose.xml b/tests/FlickerTests/manifests/AndroidManifestAppClose.xml
deleted file mode 100644
index 4cdcb90..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestAppClose.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.close">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.close"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml b/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml
deleted file mode 100644
index 659a745..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.launch">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.launch"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestIme.xml b/tests/FlickerTests/manifests/AndroidManifestIme.xml
deleted file mode 100644
index abd03af..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestIme.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.ime">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.ime"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestNotification.xml b/tests/FlickerTests/manifests/AndroidManifestNotification.xml
deleted file mode 100644
index ad33dee..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestNotification.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.close">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.notification"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml b/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml
deleted file mode 100644
index 203035d..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.quickswitch">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.quickswitch"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestRotation.xml b/tests/FlickerTests/manifests/AndroidManifestRotation.xml
deleted file mode 100644
index 2852cf2..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestRotation.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.rotation">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.rotation"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestService.xml b/tests/FlickerTests/manifests/AndroidManifestService.xml
deleted file mode 100644
index 3a7bc509..0000000
--- a/tests/FlickerTests/manifests/AndroidManifestService.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.server.wm.flicker.service">
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker.service"
-                     android:label="WindowManager Flicker Service Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/test-apps/app-helpers/Android.bp b/tests/FlickerTests/test-apps/app-helpers/Android.bp
new file mode 100644
index 0000000..fc4d71c
--- /dev/null
+++ b/tests/FlickerTests/test-apps/app-helpers/Android.bp
@@ -0,0 +1,42 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library {
+    name: "wm-flicker-common-app-helpers",
+    platform_apis: true,
+    optimize: {
+        enabled: false,
+    },
+    srcs: ["src/**/*"],
+    static_libs: [
+        "flickertestapplib",
+        "flickerlib",
+        "flickerlib-apphelpers",
+        "flickerlib-helpers",
+        "truth",
+        "app-helpers-core",
+        "wm-flicker-window-extensions",
+    ],
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AppPairsHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/AppPairsHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AppPairsHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/AppPairsHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GestureHelper.java
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GestureHelper.java
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeEditorPopupDialogAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
index e106f65..d3cee64 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
@@ -121,7 +121,7 @@
                     else -> null
                 }
             if (matcher != null && matcher.find()) {
-                return matcher.group(1).equals("VISIBLE")
+                return matcher.group(1) == "VISIBLE"
             }
         }
         return false
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LaunchBubbleHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LaunchBubbleHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LaunchBubbleHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LaunchBubbleHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MultiWindowUtils.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MultiWindowUtils.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MultiWindowUtils.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MultiWindowUtils.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
similarity index 98%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index da51eff..73cc2f2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -250,11 +250,12 @@
             launchedAppComponentMatcherOverride,
             action,
             stringExtras,
-            waitConditionsBuilder = wmHelper
-                .StateSyncBuilder()
-                .add(ConditionsFactory.isWMStateComplete())
-                .withAppTransitionIdle()
-                .add(ConditionsFactory.hasPipWindow())
+            waitConditionsBuilder =
+                wmHelper
+                    .StateSyncBuilder()
+                    .add(ConditionsFactory.isWMStateComplete())
+                    .withAppTransitionIdle()
+                    .add(ConditionsFactory.hasPipWindow())
         )
 
         wmHelper
@@ -265,8 +266,7 @@
     }
 
     /** Expand the PIP window back to full screen via intent and wait until the app is visible */
-    fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) =
-        launchViaIntent(wmHelper)
+    fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) = launchViaIntent(wmHelper)
 
     fun changeAspectRatio() {
         val intent = Intent("com.android.wm.shell.flicker.testapp.ASPECT_RATIO")
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
similarity index 100%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
rename to tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
diff --git a/tools/fonts/update_font_metadata.py b/tools/fonts/update_font_metadata.py
index c07a98a..04a5528 100755
--- a/tools/fonts/update_font_metadata.py
+++ b/tools/fonts/update_font_metadata.py
@@ -19,7 +19,7 @@
     args_parser.add_argument('--revision', help='Updated font revision. Use + to update revision based on the current revision')
     args = args_parser.parse_args()
 
-    font = ttLib.TTFont(args.input)
+    font = ttLib.TTFont(args.input, recalcTimestamp=False)
     update_font_revision(font, args.revision)
     font.save(args.output)
 
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index f293f9a..872d568 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -17,9 +17,9 @@
 
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.filters.AnnotationBasedFilter
-import com.android.hoststubgen.filters.DefaultHookInjectingFilter
 import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter
 import com.android.hoststubgen.filters.ConstantFilter
+import com.android.hoststubgen.filters.DefaultHookInjectingFilter
 import com.android.hoststubgen.filters.FilterPolicy
 import com.android.hoststubgen.filters.ImplicitOutputFilter
 import com.android.hoststubgen.filters.KeepAllClassesFilter
@@ -28,6 +28,7 @@
 import com.android.hoststubgen.filters.createFilterFromTextPolicyFile
 import com.android.hoststubgen.filters.printAsTextPolicy
 import com.android.hoststubgen.visitors.BaseAdapter
+import com.android.hoststubgen.visitors.PackageRedirectRemapper
 import org.objectweb.asm.ClassReader
 import org.objectweb.asm.ClassVisitor
 import org.objectweb.asm.ClassWriter
@@ -238,6 +239,8 @@
 
         val start = System.currentTimeMillis()
 
+        val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+
         log.withIndent {
             // Open the input jar file and process each entry.
             ZipFile(inJar).use { inZip ->
@@ -247,7 +250,7 @@
                         while (inEntries.hasMoreElements()) {
                             val entry = inEntries.nextElement()
                             convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
-                                    filter, enableChecker, classes, errors)
+                                    filter, packageRedirector, enableChecker, classes, errors)
                         }
                         log.i("Converted all entries.")
                     }
@@ -269,6 +272,7 @@
             stubOutStream: ZipOutputStream,
             implOutStream: ZipOutputStream,
             filter: OutputFilter,
+            packageRedirector: PackageRedirectRemapper,
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
@@ -285,7 +289,7 @@
             // If it's a class, convert it.
             if (name.endsWith(".class")) {
                 processSingleClass(inZip, entry, stubOutStream, implOutStream, filter,
-                        enableChecker, classes, errors)
+                        packageRedirector, enableChecker, classes, errors)
                 return
             }
 
@@ -335,37 +339,40 @@
             stubOutStream: ZipOutputStream,
             implOutStream: ZipOutputStream,
             filter: OutputFilter,
+            packageRedirector: PackageRedirectRemapper,
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
             ) {
-        val className = entry.name.replaceFirst("\\.class$".toRegex(), "")
-        val classPolicy = filter.getPolicyForClass(className)
+        val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "")
+        val classPolicy = filter.getPolicyForClass(classInternalName)
         if (classPolicy.policy == FilterPolicy.Remove) {
-            log.d("Removing class: %s %s", className, classPolicy)
+            log.d("Removing class: %s %s", classInternalName, classPolicy)
             return
         }
         // Generate stub first.
         if (classPolicy.policy.needsInStub) {
-            log.v("Creating stub class: %s Policy: %s", className, classPolicy)
+            log.v("Creating stub class: %s Policy: %s", classInternalName, classPolicy)
             log.withIndent {
                 BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
                     val newEntry = ZipEntry(entry.name)
                     stubOutStream.putNextEntry(newEntry)
-                    convertClass(/*forImpl=*/false, bis, stubOutStream, filter, enableChecker,
-                            classes, errors)
+                    convertClass(classInternalName, /*forImpl=*/false, bis,
+                            stubOutStream, filter, packageRedirector, enableChecker, classes,
+                            errors)
                     stubOutStream.closeEntry()
                 }
             }
         }
-        log.v("Creating impl class: %s Policy: %s", className, classPolicy)
+        log.v("Creating impl class: %s Policy: %s", classInternalName, classPolicy)
         if (classPolicy.policy.needsInImpl) {
             log.withIndent {
                 BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
                     val newEntry = ZipEntry(entry.name)
                     implOutStream.putNextEntry(newEntry)
-                    convertClass(/*forImpl=*/true, bis, implOutStream, filter, enableChecker,
-                            classes, errors)
+                    convertClass(classInternalName, /*forImpl=*/true, bis,
+                            implOutStream, filter, packageRedirector, enableChecker, classes,
+                            errors)
                     implOutStream.closeEntry()
                 }
             }
@@ -376,14 +383,16 @@
      * Convert a single class to either "stub" or "impl".
      */
     private fun convertClass(
-        forImpl: Boolean,
-        input: InputStream,
-        out: OutputStream,
-        filter: OutputFilter,
-        enableChecker: Boolean,
-        classes: ClassNodes,
-        errors: HostStubGenErrors,
-        ) {
+            classInternalName: String,
+            forImpl: Boolean,
+            input: InputStream,
+            out: OutputStream,
+            filter: OutputFilter,
+            packageRedirector: PackageRedirectRemapper,
+            enableChecker: Boolean,
+            classes: ClassNodes,
+            errors: HostStubGenErrors,
+            ) {
         val cr = ClassReader(input)
 
         // COMPUTE_FRAMES wouldn't be happy if code uses
@@ -398,11 +407,11 @@
         val visitorOptions = BaseAdapter.Options(
                 enablePreTrace = options.enablePreTrace,
                 enablePostTrace = options.enablePostTrace,
-                enableMethodLogging = options.enablePreTrace,
                 enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection,
                 errors = errors,
         )
-        outVisitor = BaseAdapter.getVisitor(classes, outVisitor, filter, forImpl, visitorOptions)
+        outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter,
+                packageRedirector, forImpl, visitorOptions)
 
         cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
         val data = cw.toByteArray()
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 5b7216f..d74612d 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -49,6 +49,8 @@
         var classLoadHookAnnotations: MutableSet<String> = mutableSetOf(),
         var stubStaticInitializerAnnotations: MutableSet<String> = mutableSetOf(),
 
+        var packageRedirects: MutableList<Pair<String, String>> = mutableListOf(),
+
         var defaultClassLoadHook: String? = null,
         var defaultMethodCallHook: String? = null,
 
@@ -78,6 +80,15 @@
             return this
         }
 
+        private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
+            val colon = fromColonTo.indexOf(':')
+            if ((colon < 1) || (colon + 1 >= fromColonTo.length)) {
+                throw ArgumentsException("--package-redirect must be a colon-separated string")
+            }
+            // TODO check for duplicates
+            return Pair(fromColonTo.substring(0, colon), fromColonTo.substring(colon + 1))
+        }
+
         fun parseArgs(args: Array<String>): HostStubGenOptions {
             val ret = HostStubGenOptions()
 
@@ -157,6 +168,9 @@
                         ret.stubStaticInitializerAnnotations +=
                                 ensureUniqueAnnotation(ai.nextArgRequired(arg))
 
+                    "--package-redirect" ->
+                        ret.packageRedirects += parsePackageRedirect(ai.nextArgRequired(arg))
+
                     "--default-class-load-hook" ->
                         ret.defaultClassLoadHook = ai.nextArgRequired(arg)
 
@@ -299,6 +313,7 @@
               substituteAnnotations=$substituteAnnotations,
               nativeSubstituteAnnotations=$nativeSubstituteAnnotations,
               classLoadHookAnnotations=$classLoadHookAnnotations,
+              packageRedirects=$packageRedirects,
               defaultClassLoadHook=$defaultClassLoadHook,
               defaultMethodCallHook=$defaultMethodCallHook,
               intersectStubJars=$intersectStubJars,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index 3cf9a1d..f25e862 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -30,6 +30,7 @@
 import org.objectweb.asm.FieldVisitor
 import org.objectweb.asm.MethodVisitor
 import org.objectweb.asm.Opcodes
+import org.objectweb.asm.commons.ClassRemapper
 import org.objectweb.asm.util.TraceClassVisitor
 import java.io.PrintWriter
 
@@ -49,9 +50,8 @@
             val errors: HostStubGenErrors,
             val enablePreTrace: Boolean,
             val enablePostTrace: Boolean,
-            val enableMethodLogging: Boolean,
             val enableNonStubMethodCallDetection: Boolean,
-    )
+            )
 
     protected lateinit var currentPackageName: String
     protected lateinit var currentClassName: String
@@ -219,9 +219,11 @@
 
     companion object {
         fun getVisitor(
+                classInternalName: String,
                 classes: ClassNodes,
                 nextVisitor: ClassVisitor,
                 filter: OutputFilter,
+                packageRedirector: PackageRedirectRemapper,
                 forImpl: Boolean,
                 options: Options,
         ): ClassVisitor {
@@ -229,12 +231,27 @@
 
             val verbosePrinter = PrintWriter(log.getVerbosePrintStream())
 
-            // TODO: This doesn't work yet.
-
             // Inject TraceClassVisitor for debugging.
             if (options.enablePostTrace) {
                 next = TraceClassVisitor(next, verbosePrinter)
             }
+
+            // Handle --package-redirect
+            if (!packageRedirector.isEmpty) {
+                // Don't apply the remapper on redirect-from classes.
+                // Otherwise, if the target jar actually contains the "from" classes (which
+                // may or may not be the case) they'd be renamed.
+                // But we update all references in other places, so, a method call to a "from" class
+                // would be replaced with the "to" class. All type references (e.g. variable types)
+                // will be updated too.
+                if (!packageRedirector.isTarget(classInternalName)) {
+                    next = ClassRemapper(next, packageRedirector)
+                } else {
+                    log.v("Class $classInternalName is a redirect-from class, not applying" +
+                            " --package-redirect")
+                }
+            }
+
             var ret: ClassVisitor
             if (forImpl) {
                 ret = ImplGeneratingAdapter(classes, next, filter, options)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index 3245d1b..e63efd0 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -200,7 +200,7 @@
                                 access, name, descriptor, signature, exceptions, innerVisitor)
                     }
                     else -> {
-                        throw RuntimeException("Ignored policy only allowed for void methods");
+                        throw RuntimeException("Ignored policy only allowed for void methods")
                     }
                 }
             }
@@ -408,4 +408,4 @@
                     false)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt
new file mode 100644
index 0000000..b3790e1
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.visitors
+
+import com.android.hoststubgen.asm.toJvmClassName
+import org.objectweb.asm.commons.Remapper
+
+/**
+ * A [Remapper] for `--package-redirect`
+ */
+class PackageRedirectRemapper(
+        packageRedirects: List<Pair<String, String>>,
+        ) : Remapper() {
+
+    /**
+     * Example: `dalvik/` -> `com/android/hostsubgen/substitution/dalvik/`
+     */
+    private val packageRedirectsWithSlash: List<Pair<String, String>> = packageRedirects.map {
+        p -> Pair(p.first.toJvmClassName() + "/", p.second.toJvmClassName() + "/")
+    }
+
+    /**
+     * Cache.
+     * If a class is a redirect-from class, then the "to" class name will be stored as the value.
+     * Otherwise, "" will be stored.
+     */
+    private val cache = mutableMapOf<String, String>()
+
+    /**
+     * Return whether any redirect is defined.
+     */
+    val isEmpty get() = packageRedirectsWithSlash.isEmpty()
+
+    override fun map(internalName: String?): String? {
+        if (internalName == null) {
+            return null
+        }
+        val to = mapInner(internalName)
+        return to ?: internalName
+    }
+
+    /**
+     * Internal "map" function. Unlike [map(String)], this method will return null
+     * if a class is not a redirect-from class.
+     */
+    private fun mapInner(internalName: String): String? {
+        cache[internalName]?.let {
+            return if (it.isEmpty()) null else it
+        }
+
+        var ret = ""
+        packageRedirectsWithSlash.forEach { fromTo ->
+            if (internalName.startsWith(fromTo.first)) {
+                ret = fromTo.second + internalName.substring(fromTo.first.length)
+            }
+        }
+        cache.set(internalName, ret)
+
+        return if (ret.isEmpty()) null else ret
+    }
+
+    /**
+     * Return true if a class is a redirect-from class.
+     */
+    fun isTarget(internalName: String): Boolean {
+        return mapInner(internalName) != null
+    }
+}
+
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp b/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp
index f9dc305..3dc6da3 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp
@@ -24,7 +24,8 @@
     defaults: ["hoststubgen-command-defaults"],
     cmd: hoststubgen_common_options +
         "--in-jar $(location :hoststubgen-test-tiny-framework) " +
-        "--policy-override-file $(location policy-override-tiny-framework.txt) ",
+        "--policy-override-file $(location policy-override-tiny-framework.txt) " +
+        "--package-redirect com.unsupported:com.supported ",
     srcs: [
         ":hoststubgen-test-tiny-framework",
         "policy-override-tiny-framework.txt",
@@ -61,6 +62,7 @@
     cmd: hoststubgen_common_options +
         "--in-jar $(location :hoststubgen-test-tiny-framework) " +
         "--policy-override-file $(location policy-override-tiny-framework.txt) " +
+        "--package-redirect com.unsupported:com.supported " +
 
         // More options.
         "--default-method-call-hook com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall " +
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 8158054..f627c6e 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -1759,3 +1759,138 @@
   public static #x= #x of #x;          // StaticNestedClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
   public #x= #x of #x;                 // InnerClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$InnerClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
   #x;                                    // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
+## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.class
+  Compiled from "TinyFrameworkPackageRedirect.java"
+public class com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                         // com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 2
+  public com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=1, locals=1, args_size=1
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0       5     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect;
+
+  public static int foo(int);
+    descriptor: (I)I
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                  // class com/unsupported/UnsupportedClass
+         x: dup
+         x: iload_0
+         x: invokespecial #x                  // Method com/unsupported/UnsupportedClass."<init>":(I)V
+         x: invokevirtual #x                 // Method com/unsupported/UnsupportedClass.getValue:()I
+        x: ireturn
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      12     0 value   I
+}
+SourceFile: "TinyFrameworkPackageRedirect.java"
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/supported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.supported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/supported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 1, methods: 2, attributes: 2
+  private final int mValue;
+    descriptor: I
+    flags: (0x0012) ACC_PRIVATE, ACC_FINAL
+
+  public com.supported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=2, locals=2, args_size=2
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iload_1
+         x: putfield      #x                  // Field mValue:I
+         x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      10     0  this   Lcom/supported/UnsupportedClass;
+            0      10     1 value   I
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=1, locals=1, args_size=1
+         x: aload_0
+         x: getfield      #x                  // Field mValue:I
+         x: ireturn
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0       5     0  this   Lcom/supported/UnsupportedClass;
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassKeep
+## Class: com/unsupported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.unsupported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                         // com/unsupported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 2
+  public com.unsupported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=2, args_size=2
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: new           #x                  // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                  // String This class is not supported
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+        x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      14     0  this   Lcom/unsupported/UnsupportedClass;
+            0      14     1 value   I
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                  // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                  // String This class is not supported
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      10     0  this   Lcom/unsupported/UnsupportedClass;
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
index f405cfc..d7f0149 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
@@ -869,3 +869,83 @@
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.class
+  Compiled from "TinyFrameworkPackageRedirect.java"
+public class com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 3
+  public com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+
+  public static int foo(int);
+    descriptor: (I)I
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+}
+SourceFile: "TinyFrameworkPackageRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/unsupported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.unsupported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/unsupported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 3
+  public com.unsupported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=2, args_size=2
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
index 9149ee3..131e0b1 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
@@ -1808,3 +1808,163 @@
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.class
+  Compiled from "TinyFrameworkPackageRedirect.java"
+public class com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 3
+  public com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=1, locals=1, args_size=1
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0       5     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect;
+
+  public static int foo(int);
+    descriptor: (I)I
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class com/supported/UnsupportedClass
+         x: dup
+         x: iload_0
+         x: invokespecial #x                 // Method com/supported/UnsupportedClass."<init>":(I)V
+         x: invokevirtual #x                 // Method com/supported/UnsupportedClass.getValue:()I
+        x: ireturn
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      12     0 value   I
+}
+SourceFile: "TinyFrameworkPackageRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/supported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.supported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/supported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 1, methods: 2, attributes: 3
+  private final int mValue;
+    descriptor: I
+    flags: (0x0012) ACC_PRIVATE, ACC_FINAL
+
+  public com.supported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=2, args_size=2
+         x: ldc           #x                 // String com/supported/UnsupportedClass
+         x: ldc           #x                 // String <init>
+         x: ldc           #x                 // String (I)V
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+        x: aload_0
+        x: iload_1
+        x: putfield      #x                 // Field mValue:I
+        x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           15      10     0  this   Lcom/supported/UnsupportedClass;
+           15      10     1 value   I
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                 // String com/supported/UnsupportedClass
+         x: ldc           #x                 // String getValue
+         x: ldc           #x                 // String ()I
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: getfield      #x                 // Field mValue:I
+        x: ireturn
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           15       5     0  this   Lcom/supported/UnsupportedClass;
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassKeep
+## Class: com/unsupported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.unsupported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/unsupported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 3
+  public com.unsupported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=2, args_size=2
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String This class is not supported
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+        x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      14     0  this   Lcom/unsupported/UnsupportedClass;
+            0      14     1 value   I
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String This class is not supported
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      10     0  this   Lcom/unsupported/UnsupportedClass;
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
index f405cfc..d7f0149 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
@@ -869,3 +869,83 @@
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.class
+  Compiled from "TinyFrameworkPackageRedirect.java"
+public class com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 3
+  public com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+
+  public static int foo(int);
+    descriptor: (I)I
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+}
+SourceFile: "TinyFrameworkPackageRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/unsupported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.unsupported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/unsupported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 2, attributes: 3
+  public com.unsupported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=2, args_size=2
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=3, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
index 96bfc38..3318c7d 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
@@ -2377,3 +2377,223 @@
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.class
+  Compiled from "TinyFrameworkPackageRedirect.java"
+public class com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 3, attributes: 3
+  private static {};
+    descriptor: ()V
+    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+    Code:
+      stack=2, locals=0, args_size=0
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: return
+
+  public com.android.hoststubgen.test.tinyframework.TinyFrameworkPackageRedirect();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+         x: ldc           #x                 // String <init>
+         x: ldc           #x                 // String ()V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+        x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           11       5     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect;
+
+  public static int foo(int);
+    descriptor: (I)I
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect
+         x: ldc           #x                 // String foo
+         x: ldc           #x                 // String (I)I
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: new           #x                 // class com/supported/UnsupportedClass
+        x: dup
+        x: iload_0
+        x: invokespecial #x                 // Method com/supported/UnsupportedClass."<init>":(I)V
+        x: invokevirtual #x                 // Method com/supported/UnsupportedClass.getValue:()I
+        x: ireturn
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           11      12     0 value   I
+}
+SourceFile: "TinyFrameworkPackageRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/supported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.supported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/supported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 1, methods: 3, attributes: 3
+  private final int mValue;
+    descriptor: I
+    flags: (0x0012) ACC_PRIVATE, ACC_FINAL
+
+  private static {};
+    descriptor: ()V
+    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+    Code:
+      stack=2, locals=0, args_size=0
+         x: ldc           #x                  // class com/supported/UnsupportedClass
+         x: ldc           #x                  // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: return
+
+  public com.supported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=2, args_size=2
+         x: ldc           #x                  // class com/supported/UnsupportedClass
+         x: ldc           #x                 // String <init>
+         x: ldc           #x                 // String (I)V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: ldc           #x                 // String com/supported/UnsupportedClass
+        x: ldc           #x                 // String <init>
+        x: ldc           #x                 // String (I)V
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+        x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+        x: aload_0
+        x: iload_1
+        x: putfield      #x                 // Field mValue:I
+        x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           26      10     0  this   Lcom/supported/UnsupportedClass;
+           26      10     1 value   I
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/supported/UnsupportedClass
+         x: ldc           #x                 // String getValue
+         x: ldc           #x                 // String ()I
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: ldc           #x                 // String com/supported/UnsupportedClass
+        x: ldc           #x                 // String getValue
+        x: ldc           #x                 // String ()I
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+        x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: getfield      #x                 // Field mValue:I
+        x: ireturn
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           26       5     0  this   Lcom/supported/UnsupportedClass;
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassKeep
+## Class: com/unsupported/UnsupportedClass.class
+  Compiled from "UnsupportedClass.java"
+public class com.unsupported.UnsupportedClass
+  minor version: 0
+  major version: 61
+  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+  this_class: #x                          // com/unsupported/UnsupportedClass
+  super_class: #x                         // java/lang/Object
+  interfaces: 0, fields: 0, methods: 3, attributes: 3
+  private static {};
+    descriptor: ()V
+    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+    Code:
+      stack=2, locals=0, args_size=0
+         x: ldc           #x                  // class com/unsupported/UnsupportedClass
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: return
+
+  public com.unsupported.UnsupportedClass(int);
+    descriptor: (I)V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=2, args_size=2
+         x: ldc           #x                  // class com/unsupported/UnsupportedClass
+         x: ldc           #x                 // String <init>
+         x: ldc           #x                 // String (I)V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+        x: new           #x                 // class java/lang/RuntimeException
+        x: dup
+        x: ldc           #x                 // String This class is not supported
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+        x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           11      14     0  this   Lcom/unsupported/UnsupportedClass;
+           11      14     1 value   I
+
+  public int getValue();
+    descriptor: ()I
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/unsupported/UnsupportedClass
+         x: ldc           #x                 // String getValue
+         x: ldc           #x                 // String ()I
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: new           #x                 // class java/lang/RuntimeException
+        x: dup
+        x: ldc           #x                 // String This class is not supported
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+        x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           11      10     0  this   Lcom/unsupported/UnsupportedClass;
+}
+SourceFile: "UnsupportedClass.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
+RuntimeInvisibleAnnotations:
+  x: #x()
+    android.hosttest.annotation.HostSideTestWholeClassStub
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh b/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh
index 523106f..e212890 100755
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh
@@ -92,6 +92,7 @@
     --policy-override-file policy-override-tiny-framework.txt \
     --gen-keep-all-file out/tiny-framework_keep_all.txt \
     --gen-input-dump-file out/tiny-framework_dump.txt \
+    --package-redirect com.unsupported:com.supported \
     $HOSTSTUBGEN_OPTS
 
 # Extract the jar files, so we can look into them.
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.java
new file mode 100644
index 0000000..a82be54
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkPackageRedirect.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.test.tinyframework;
+
+import android.hosttest.annotation.HostSideTestWholeClassStub;
+
+@HostSideTestWholeClassStub
+public class TinyFrameworkPackageRedirect {
+    /**
+     * A method that uses "unsupported" class. HostStubGen will redirect them to the "supported"
+     * one (because of --package-redirect), so this test will pass.
+     */
+    public static int foo(int value) {
+        // This method throws, so it's not callable as-is. But HostStubGen
+        // will rewrite it, it will actually work.
+        return new com.unsupported.UnsupportedClass(value).getValue();
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/supported/UnsupportedClass.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/supported/UnsupportedClass.java
new file mode 100644
index 0000000..fa58664
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/supported/UnsupportedClass.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.supported;
+
+import android.hosttest.annotation.HostSideTestWholeClassKeep;
+
+// Used for testing --package-redirect.
+@HostSideTestWholeClassKeep
+public class UnsupportedClass {
+    private final int mValue;
+
+    public UnsupportedClass(int value) {
+        mValue = value;
+    }
+
+    public int getValue() {
+        return mValue;
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/unsupported/UnsupportedClass.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/unsupported/UnsupportedClass.java
new file mode 100644
index 0000000..0409b02
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/unsupported/UnsupportedClass.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.unsupported;
+
+import android.hosttest.annotation.HostSideTestWholeClassStub;
+
+// Used for testing --package-redirect.
+@HostSideTestWholeClassStub
+public class UnsupportedClass {
+    public UnsupportedClass(int value) {
+        throw new RuntimeException("This class is not supported");
+    }
+
+    public int getValue() {
+        throw new RuntimeException("This class is not supported");
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index dcf60cf..29aabc7 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -194,4 +194,9 @@
         m.invoke(fd, 0);
         assertThat(f.get(fd)).isEqualTo(0);
     }
+
+    @Test
+    public void testPackageRedirect() throws Exception {
+        assertThat(TinyFrameworkPackageRedirect.foo(1)).isEqualTo(1);
+    }
 }