Merge "Disable Desktop Windowing for devices with smaller screen sizes" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index eed248b..a80194c 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -335,6 +335,17 @@
     mode: "exported",
 }
 
+cc_aconfig_library {
+    name: "android.os.flags-aconfig-cc",
+    aconfig_declarations: "android.os.flags-aconfig",
+}
+
+cc_aconfig_library {
+    name: "android.os.flags-aconfig-cc-test",
+    aconfig_declarations: "android.os.flags-aconfig",
+    mode: "test",
+}
+
 // VirtualDeviceManager
 cc_aconfig_library {
     name: "android.companion.virtualdevice.flags-aconfig-cc",
@@ -492,6 +503,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "android.content.res.flags-aconfig-java-host",
+    aconfig_declarations: "android.content.res.flags-aconfig",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Media BetterTogether
 aconfig_declarations {
     name: "com.android.media.flags.bettertogether-aconfig",
diff --git a/PACKAGE_MANAGER_OWNERS b/PACKAGE_MANAGER_OWNERS
index eb5842b..45719a7 100644
--- a/PACKAGE_MANAGER_OWNERS
+++ b/PACKAGE_MANAGER_OWNERS
@@ -1,3 +1,6 @@
+# Bug component: 36137
+# Bug template url: https://b.corp.google.com/issues/new?component=36137&template=198919
+
 alexbuy@google.com
 patb@google.com
 schfan@google.com
\ No newline at end of file
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index e96d07f..ee9400f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -46,8 +46,11 @@
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccSlotMapping;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
+import android.util.IntArray;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
@@ -68,6 +71,8 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -1620,9 +1625,21 @@
         private final Object mSatLock = new Object();
 
         private DeviceIdleInternal mDeviceIdleInternal;
+        private TelephonyManager mTelephonyManager;
+
+        private final boolean mHasFeatureTelephonySubscription;
 
         /** Set of all apps that have been deemed special, keyed by user ID. */
         private final SparseSetArray<String> mSpecialApps = new SparseSetArray<>();
+        /**
+         * Set of carrier privileged apps, keyed by the logical ID of the SIM their privileged
+         * for.
+         */
+        @GuardedBy("mSatLock")
+        private final SparseSetArray<String> mCarrierPrivilegedApps = new SparseSetArray<>();
+        @GuardedBy("mSatLock")
+        private final SparseArray<LogicalIndexCarrierPrivilegesCallback>
+                mCarrierPrivilegedCallbacks = new SparseArray<>();
         @GuardedBy("mSatLock")
         private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>();
 
@@ -1630,6 +1647,10 @@
             @Override
             public void onReceive(Context context, Intent intent) {
                 switch (intent.getAction()) {
+                    case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
+                        updateCarrierPrivilegedCallbackRegistration();
+                        break;
+
                     case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
                         mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache);
                         break;
@@ -1637,6 +1658,11 @@
             }
         };
 
+        SpecialAppTracker() {
+            mHasFeatureTelephonySubscription = mContext.getPackageManager()
+                    .hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION);
+        }
+
         public boolean isSpecialApp(final int userId, @NonNull String packageName) {
             synchronized (mSatLock) {
                 if (mSpecialApps.contains(UserHandle.USER_ALL, packageName)) {
@@ -1654,6 +1680,12 @@
                 if (mPowerAllowlistedApps.contains(packageName)) {
                     return true;
                 }
+                for (int l = mCarrierPrivilegedApps.size() - 1; l >= 0; --l) {
+                    if (mCarrierPrivilegedApps.contains(
+                            mCarrierPrivilegedApps.keyAt(l), packageName)) {
+                        return true;
+                    }
+                }
             }
             return false;
         }
@@ -1669,9 +1701,12 @@
 
         private void onSystemServicesReady() {
             mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
+            mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
 
             synchronized (mLock) {
                 if (mFlexibilityEnabled) {
+                    mHandler.post(
+                            SpecialAppTracker.this::updateCarrierPrivilegedCallbackRegistration);
                     mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache);
                 }
             }
@@ -1686,6 +1721,13 @@
         private void startTracking() {
             IntentFilter filter = new IntentFilter(
                     PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+
+            if (mHasFeatureTelephonySubscription) {
+                filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
+
+                updateCarrierPrivilegedCallbackRegistration();
+            }
+
             mContext.registerReceiver(mBroadcastReceiver, filter);
 
             updatePowerAllowlistCache();
@@ -1695,11 +1737,63 @@
             mContext.unregisterReceiver(mBroadcastReceiver);
 
             synchronized (mSatLock) {
+                mCarrierPrivilegedApps.clear();
                 mPowerAllowlistedApps.clear();
                 mSpecialApps.clear();
+
+                for (int i = mCarrierPrivilegedCallbacks.size() - 1; i >= 0; --i) {
+                    mTelephonyManager.unregisterCarrierPrivilegesCallback(
+                            mCarrierPrivilegedCallbacks.valueAt(i));
+                }
+                mCarrierPrivilegedCallbacks.clear();
             }
         }
 
+        private void updateCarrierPrivilegedCallbackRegistration() {
+            if (mTelephonyManager == null) {
+                return;
+            }
+            if (!mHasFeatureTelephonySubscription) {
+                return;
+            }
+
+            Collection<UiccSlotMapping> simSlotMapping = mTelephonyManager.getSimSlotMapping();
+            final ArraySet<String> changedPkgs = new ArraySet<>();
+            synchronized (mSatLock) {
+                final IntArray callbacksToRemove = new IntArray();
+                for (int i = mCarrierPrivilegedCallbacks.size() - 1; i >= 0; --i) {
+                    callbacksToRemove.add(mCarrierPrivilegedCallbacks.keyAt(i));
+                }
+                for (UiccSlotMapping mapping : simSlotMapping) {
+                    final int logicalIndex = mapping.getLogicalSlotIndex();
+                    if (mCarrierPrivilegedCallbacks.contains(logicalIndex)) {
+                        // Callback already exists. No need to create a new one or remove it.
+                        callbacksToRemove.remove(logicalIndex);
+                        continue;
+                    }
+                    final LogicalIndexCarrierPrivilegesCallback callback =
+                            new LogicalIndexCarrierPrivilegesCallback(logicalIndex);
+                    mCarrierPrivilegedCallbacks.put(logicalIndex, callback);
+                    // Upon registration, the callbacks will be called with the current list of
+                    // apps, so there's no need to query the app list synchronously.
+                    mTelephonyManager.registerCarrierPrivilegesCallback(logicalIndex,
+                            AppSchedulingModuleThread.getExecutor(), callback);
+                }
+
+                for (int i = callbacksToRemove.size() - 1; i >= 0; --i) {
+                    final int logicalIndex = callbacksToRemove.get(i);
+                    final LogicalIndexCarrierPrivilegesCallback callback =
+                            mCarrierPrivilegedCallbacks.get(logicalIndex);
+                    mTelephonyManager.unregisterCarrierPrivilegesCallback(callback);
+                    mCarrierPrivilegedCallbacks.remove(logicalIndex);
+                    changedPkgs.addAll(mCarrierPrivilegedApps.get(logicalIndex));
+                    mCarrierPrivilegedApps.remove(logicalIndex);
+                }
+            }
+
+            updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
+        }
+
         /**
          * Update the processed special app set for the specified user ID, only looking at the
          * specified set of apps. This method must <b>NEVER</b> be called while holding
@@ -1762,18 +1856,65 @@
             updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
         }
 
+        class LogicalIndexCarrierPrivilegesCallback implements
+                TelephonyManager.CarrierPrivilegesCallback {
+            public final int logicalIndex;
+
+            LogicalIndexCarrierPrivilegesCallback(int logicalIndex) {
+                this.logicalIndex = logicalIndex;
+            }
+
+            @Override
+            public void onCarrierPrivilegesChanged(@NonNull Set<String> privilegedPackageNames,
+                    @NonNull Set<Integer> privilegedUids) {
+                final ArraySet<String> changedPkgs = new ArraySet<>();
+                synchronized (mSatLock) {
+                    final ArraySet<String> oldPrivilegedSet =
+                            mCarrierPrivilegedApps.get(logicalIndex);
+                    if (oldPrivilegedSet != null) {
+                        changedPkgs.addAll(oldPrivilegedSet);
+                        mCarrierPrivilegedApps.remove(logicalIndex);
+                    }
+                    for (String pkgName : privilegedPackageNames) {
+                        mCarrierPrivilegedApps.add(logicalIndex, pkgName);
+                        if (!changedPkgs.remove(pkgName)) {
+                            // The package wasn't in the previous set of privileged apps. Add it
+                            // since its state has changed.
+                            changedPkgs.add(pkgName);
+                        }
+                    }
+                }
+
+                // The carrier privileged list doesn't provide a simple userId correlation,
+                // so for now, use USER_ALL for these packages.
+                // TODO(141645789): use the UID list to narrow down to specific userIds
+                updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
+            }
+        }
+
         public void dump(@NonNull IndentingPrintWriter pw) {
             pw.println("Special apps:");
             pw.increaseIndent();
 
             synchronized (mSatLock) {
                 for (int u = 0; u < mSpecialApps.size(); ++u) {
+                    pw.print("User ");
                     pw.print(mSpecialApps.keyAt(u));
                     pw.print(": ");
                     pw.println(mSpecialApps.valuesAt(u));
                 }
 
                 pw.println();
+                pw.println("Carrier privileged packages:");
+                pw.increaseIndent();
+                for (int i = 0; i < mCarrierPrivilegedApps.size(); ++i) {
+                    pw.print(mCarrierPrivilegedApps.keyAt(i));
+                    pw.print(": ");
+                    pw.println(mCarrierPrivilegedApps.valuesAt(i));
+                }
+                pw.decreaseIndent();
+
+                pw.println();
                 pw.print("Power allowlisted packages: ");
                 pw.println(mPowerAllowlistedApps);
             }
diff --git a/api/Android.bp b/api/Android.bp
index 8e06366..093ee4b 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -298,6 +298,28 @@
     "org.xmlpull",
 ]
 
+// These are libs from framework-internal-utils that are required (i.e. being referenced)
+// from framework-non-updatable-sources. Add more here when there's a need.
+// DO NOT add the entire framework-internal-utils. It might cause unnecessary circular
+// dependencies gets bigger.
+android_non_updatable_stubs_libs = [
+    "android.hardware.cas-V1.2-java",
+    "android.hardware.health-V1.0-java-constants",
+    "android.hardware.thermal-V1.0-java-constants",
+    "android.hardware.thermal-V2.0-java",
+    "android.hardware.tv.input-V1.0-java-constants",
+    "android.hardware.usb-V1.0-java-constants",
+    "android.hardware.usb-V1.1-java-constants",
+    "android.hardware.usb.gadget-V1.0-java",
+    "android.hardware.vibrator-V1.3-java",
+    "framework-protos",
+]
+
+java_defaults {
+    name: "android-non-updatable-stubs-libs-defaults",
+    libs: android_non_updatable_stubs_libs,
+}
+
 // Defaults for all stubs that include the non-updatable framework. These defaults do not include
 // module symbols, so will not compile correctly on their own. Users must add module APIs to the
 // classpath (or sources) somehow.
@@ -329,18 +351,7 @@
     // from framework-non-updatable-sources. Add more here when there's a need.
     // DO NOT add the entire framework-internal-utils. It might cause unnecessary circular
     // dependencies gets bigger.
-    libs: [
-        "android.hardware.cas-V1.2-java",
-        "android.hardware.health-V1.0-java-constants",
-        "android.hardware.thermal-V1.0-java-constants",
-        "android.hardware.thermal-V2.0-java",
-        "android.hardware.tv.input-V1.0-java-constants",
-        "android.hardware.usb-V1.0-java-constants",
-        "android.hardware.usb-V1.1-java-constants",
-        "android.hardware.usb.gadget-V1.0-java",
-        "android.hardware.vibrator-V1.3-java",
-        "framework-protos",
-    ],
+    libs: android_non_updatable_stubs_libs,
     flags: [
         "--error NoSettingsProvider",
         "--error UnhiddenSystemApi",
diff --git a/cmds/telecom/Android.bp b/cmds/telecom/Android.bp
index be02710..494d2ae3 100644
--- a/cmds/telecom/Android.bp
+++ b/cmds/telecom/Android.bp
@@ -21,5 +21,8 @@
 java_binary {
     name: "telecom",
     wrapper: "telecom.sh",
-    srcs: ["**/*.java"],
+    srcs: [
+        ":telecom-shell-commands-src",
+        "**/*.java",
+    ],
 }
diff --git a/cmds/telecom/src/com/android/commands/telecom/Telecom.java b/cmds/telecom/src/com/android/commands/telecom/Telecom.java
index 1488e14cf..50af5a7 100644
--- a/cmds/telecom/src/com/android/commands/telecom/Telecom.java
+++ b/cmds/telecom/src/com/android/commands/telecom/Telecom.java
@@ -17,30 +17,22 @@
 package com.android.commands.telecom;
 
 import android.app.ActivityThread;
-import android.content.ComponentName;
 import android.content.Context;
-import android.net.Uri;
-import android.os.IUserManager;
 import android.os.Looper;
-import android.os.Process;
-import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.UserHandle;
-import android.sysprop.TelephonyProperties;
-import android.telecom.Log;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
 
-import com.android.internal.os.BaseCommand;
 import com.android.internal.telecom.ITelecomService;
+import com.android.server.telecom.TelecomShellCommand;
 
-import java.io.PrintStream;
-import java.util.Arrays;
-import java.util.stream.Collectors;
+import java.io.FileDescriptor;
 
-public final class Telecom extends BaseCommand {
+/**
+ * @deprecated Use {@code com.android.server.telecom.TelecomShellCommand} instead and execute the
+ * shell command using {@code adb shell cmd telecom...}. This is only here for backwards
+ * compatibility reasons.
+ */
+@Deprecated
+public final class Telecom {
 
     /**
      * Command-line entry point.
@@ -52,458 +44,11 @@
         // TODO: Do it in zygote and RuntimeInit. b/148897549
         ActivityThread.initializeMainlineModules();
 
-      (new Telecom()).run(args);
-    }
-    private static final String CALLING_PACKAGE = Telecom.class.getPackageName();
-    private static final String COMMAND_SET_PHONE_ACCOUNT_ENABLED = "set-phone-account-enabled";
-    private static final String COMMAND_SET_PHONE_ACCOUNT_DISABLED = "set-phone-account-disabled";
-    private static final String COMMAND_REGISTER_PHONE_ACCOUNT = "register-phone-account";
-    private static final String COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT =
-            "set-user-selected-outgoing-phone-account";
-    private static final String COMMAND_REGISTER_SIM_PHONE_ACCOUNT = "register-sim-phone-account";
-    private static final String COMMAND_SET_TEST_CALL_REDIRECTION_APP = "set-test-call-redirection-app";
-    private static final String COMMAND_SET_TEST_CALL_SCREENING_APP = "set-test-call-screening-app";
-    private static final String COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP =
-            "add-or-remove-call-companion-app";
-    private static final String COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT =
-            "set-phone-acct-suggestion-component";
-    private static final String COMMAND_UNREGISTER_PHONE_ACCOUNT = "unregister-phone-account";
-    private static final String COMMAND_SET_CALL_DIAGNOSTIC_SERVICE = "set-call-diagnostic-service";
-    private static final String COMMAND_SET_DEFAULT_DIALER = "set-default-dialer";
-    private static final String COMMAND_GET_DEFAULT_DIALER = "get-default-dialer";
-    private static final String COMMAND_STOP_BLOCK_SUPPRESSION = "stop-block-suppression";
-    private static final String COMMAND_CLEANUP_STUCK_CALLS = "cleanup-stuck-calls";
-    private static final String COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS =
-            "cleanup-orphan-phone-accounts";
-    private static final String COMMAND_RESET_CAR_MODE = "reset-car-mode";
-    private static final String COMMAND_IS_NON_IN_CALL_SERVICE_BOUND =
-            "is-non-ui-in-call-service-bound";
-
-    /**
-     * Change the system dialer package name if a package name was specified,
-     * Example: adb shell telecom set-system-dialer <PACKAGE>
-     *
-     * Restore it to the default if if argument is "default" or no argument is passed.
-     * Example: adb shell telecom set-system-dialer default
-     */
-    private static final String COMMAND_SET_SYSTEM_DIALER = "set-system-dialer";
-    private static final String COMMAND_GET_SYSTEM_DIALER = "get-system-dialer";
-    private static final String COMMAND_WAIT_ON_HANDLERS = "wait-on-handlers";
-    private static final String COMMAND_SET_SIM_COUNT = "set-sim-count";
-    private static final String COMMAND_GET_SIM_CONFIG = "get-sim-config";
-    private static final String COMMAND_GET_MAX_PHONES = "get-max-phones";
-    private static final String COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER =
-            "set-test-emergency-phone-account-package-filter";
-    /**
-     * Command used to emit a distinct "mark" in the logs.
-     */
-    private static final String COMMAND_LOG_MARK = "log-mark";
-
-    private ComponentName mComponent;
-    private String mAccountId;
-    private ITelecomService mTelecomService;
-    private TelephonyManager mTelephonyManager;
-    private IUserManager mUserManager;
-
-    @Override
-    public void onShowUsage(PrintStream out) {
-        out.println("usage: telecom [subcommand] [options]\n"
-                + "usage: telecom set-phone-account-enabled <COMPONENT> <ID> <USER_SN>\n"
-                + "usage: telecom set-phone-account-disabled <COMPONENT> <ID> <USER_SN>\n"
-                + "usage: telecom register-phone-account <COMPONENT> <ID> <USER_SN> <LABEL>\n"
-                + "usage: telecom register-sim-phone-account [-e] <COMPONENT> <ID> <USER_SN>"
-                        + " <LABEL>: registers a PhoneAccount with CAPABILITY_SIM_SUBSCRIPTION"
-                        + " and optionally CAPABILITY_PLACE_EMERGENCY_CALLS if \"-e\" is provided\n"
-                + "usage: telecom set-user-selected-outgoing-phone-account [-e] <COMPONENT> <ID> "
-                + "<USER_SN>\n"
-                + "usage: telecom set-test-call-redirection-app <PACKAGE>\n"
-                + "usage: telecom set-test-call-screening-app <PACKAGE>\n"
-                + "usage: telecom set-phone-acct-suggestion-component <COMPONENT>\n"
-                + "usage: telecom add-or-remove-call-companion-app <PACKAGE> <1/0>\n"
-                + "usage: telecom register-sim-phone-account <COMPONENT> <ID> <USER_SN>"
-                + " <LABEL> <ADDRESS>\n"
-                + "usage: telecom unregister-phone-account <COMPONENT> <ID> <USER_SN>\n"
-                + "usage: telecom set-call-diagnostic-service <PACKAGE>\n"
-                + "usage: telecom set-default-dialer <PACKAGE>\n"
-                + "usage: telecom get-default-dialer\n"
-                + "usage: telecom get-system-dialer\n"
-                + "usage: telecom wait-on-handlers\n"
-                + "usage: telecom set-sim-count <COUNT>\n"
-                + "usage: telecom get-sim-config\n"
-                + "usage: telecom get-max-phones\n"
-                + "usage: telecom stop-block-suppression: Stop suppressing the blocked number"
-                        + " provider after a call to emergency services.\n"
-                + "usage: telecom cleanup-stuck-calls: Clear any disconnected calls that have"
-                + " gotten wedged in Telecom.\n"
-                + "usage: telecom cleanup-orphan-phone-accounts: remove any phone accounts that"
-                + " no longer have a valid UserHandle or accounts that no longer belongs to an"
-                + " installed package.\n"
-                + "usage: telecom set-emer-phone-account-filter <PACKAGE>\n"
-                + "\n"
-                + "telecom set-phone-account-enabled: Enables the given phone account, if it has"
-                        + " already been registered with Telecom.\n"
-                + "\n"
-                + "telecom set-phone-account-disabled: Disables the given phone account, if it"
-                        + " has already been registered with telecom.\n"
-                + "\n"
-                + "telecom set-call-diagnostic-service: overrides call diagnostic service.\n"
-                + "telecom set-default-dialer: Sets the override default dialer to the given"
-                        + " component; this will override whatever the dialer role is set to.\n"
-                + "\n"
-                + "telecom get-default-dialer: Displays the current default dialer.\n"
-                + "\n"
-                + "telecom get-system-dialer: Displays the current system dialer.\n"
-                + "telecom set-system-dialer: Set the override system dialer to the given"
-                        + " component. To remove the override, send \"default\"\n"
-                + "\n"
-                + "telecom wait-on-handlers: Wait until all handlers finish their work.\n"
-                + "\n"
-                + "telecom set-sim-count: Set num SIMs (2 for DSDS, 1 for single SIM."
-                        + " This may restart the device.\n"
-                + "\n"
-                + "telecom get-sim-config: Get the mSIM config string. \"DSDS\" for DSDS mode,"
-                        + " or \"\" for single SIM\n"
-                + "\n"
-                + "telecom get-max-phones: Get the max supported phones from the modem.\n"
-                + "telecom set-test-emergency-phone-account-package-filter <PACKAGE>: sets a"
-                        + " package name that will be used for test emergency calls. To clear,"
-                        + " send an empty package name. Real emergency calls will still be placed"
-                        + " over Telephony.\n"
-                + "telecom log-mark <MESSAGE>: emits a message into the telecom logs.  Useful for "
-                        + "testers to indicate where in the logs various test steps take place.\n"
-                + "telecom is-non-ui-in-call-service-bound <PACKAGE>: queries a particular "
-                + "non-ui-InCallService in InCallController to determine if it is bound \n"
-        );
-    }
-
-    @Override
-    public void onRun() throws Exception {
-        mTelecomService = ITelecomService.Stub.asInterface(
-                ServiceManager.getService(Context.TELECOM_SERVICE));
-        if (mTelecomService == null) {
-            Log.w(this, "onRun: Can't access telecom manager.");
-            showError("Error: Could not access the Telecom Manager. Is the system running?");
-            return;
-        }
-
         Looper.prepareMainLooper();
+        ITelecomService service = ITelecomService.Stub.asInterface(
+                ServiceManager.getService(Context.TELECOM_SERVICE));
         Context context = ActivityThread.systemMain().getSystemContext();
-        mTelephonyManager = context.getSystemService(TelephonyManager.class);
-        if (mTelephonyManager == null) {
-            Log.w(this, "onRun: Can't access telephony service.");
-            showError("Error: Could not access the Telephony Service. Is the system running?");
-            return;
-        }
-
-        mUserManager = IUserManager.Stub
-                .asInterface(ServiceManager.getService(Context.USER_SERVICE));
-        if (mUserManager == null) {
-            Log.w(this, "onRun: Can't access user manager.");
-            showError("Error: Could not access the User Manager. Is the system running?");
-            return;
-        }
-        Log.i(this, "onRun: parsing command.");
-        String command = nextArgRequired();
-        switch (command) {
-            case COMMAND_SET_PHONE_ACCOUNT_ENABLED:
-                runSetPhoneAccountEnabled(true);
-                break;
-            case COMMAND_SET_PHONE_ACCOUNT_DISABLED:
-                runSetPhoneAccountEnabled(false);
-                break;
-            case COMMAND_REGISTER_PHONE_ACCOUNT:
-                runRegisterPhoneAccount();
-                break;
-            case COMMAND_SET_TEST_CALL_REDIRECTION_APP:
-                runSetTestCallRedirectionApp();
-                break;
-            case COMMAND_SET_TEST_CALL_SCREENING_APP:
-                runSetTestCallScreeningApp();
-                break;
-            case COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP:
-                runAddOrRemoveCallCompanionApp();
-                break;
-            case COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT:
-                runSetTestPhoneAcctSuggestionComponent();
-                break;
-            case COMMAND_SET_CALL_DIAGNOSTIC_SERVICE:
-                runSetCallDiagnosticService();
-                break;
-            case COMMAND_REGISTER_SIM_PHONE_ACCOUNT:
-                runRegisterSimPhoneAccount();
-                break;
-            case COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT:
-                runSetUserSelectedOutgoingPhoneAccount();
-                break;
-            case COMMAND_UNREGISTER_PHONE_ACCOUNT:
-                runUnregisterPhoneAccount();
-                break;
-            case COMMAND_STOP_BLOCK_SUPPRESSION:
-                runStopBlockSuppression();
-                break;
-            case COMMAND_CLEANUP_STUCK_CALLS:
-                runCleanupStuckCalls();
-                break;
-            case COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS:
-                runCleanupOrphanPhoneAccounts();
-                break;
-            case COMMAND_RESET_CAR_MODE:
-                runResetCarMode();
-                break;
-            case COMMAND_SET_DEFAULT_DIALER:
-                runSetDefaultDialer();
-                break;
-            case COMMAND_GET_DEFAULT_DIALER:
-                runGetDefaultDialer();
-                break;
-            case COMMAND_SET_SYSTEM_DIALER:
-                runSetSystemDialer();
-                break;
-            case COMMAND_GET_SYSTEM_DIALER:
-                runGetSystemDialer();
-                break;
-            case COMMAND_WAIT_ON_HANDLERS:
-                runWaitOnHandler();
-                break;
-            case COMMAND_SET_SIM_COUNT:
-                runSetSimCount();
-                break;
-            case COMMAND_GET_SIM_CONFIG:
-                runGetSimConfig();
-                break;
-            case COMMAND_GET_MAX_PHONES:
-                runGetMaxPhones();
-                break;
-            case COMMAND_IS_NON_IN_CALL_SERVICE_BOUND:
-                runIsNonUiInCallServiceBound();
-                break;
-            case COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER:
-                runSetEmergencyPhoneAccountPackageFilter();
-                break;
-            case COMMAND_LOG_MARK:
-                runLogMark();
-                break;
-            default:
-                Log.w(this, "onRun: unknown command: %s", command);
-                throw new IllegalArgumentException ("unknown command '" + command + "'");
-        }
-    }
-
-    private void runSetPhoneAccountEnabled(boolean enabled) throws RemoteException {
-        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
-        final boolean success =  mTelecomService.enablePhoneAccount(handle, enabled);
-        if (success) {
-            System.out.println("Success - " + handle + (enabled ? " enabled." : " disabled."));
-        } else {
-            System.out.println("Error - is " + handle + " a valid PhoneAccount?");
-        }
-    }
-
-    private void runRegisterPhoneAccount() throws RemoteException {
-        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
-        final String label = nextArgRequired();
-        PhoneAccount account = PhoneAccount.builder(handle, label)
-                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
-        mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
-        System.out.println("Success - " + handle + " registered.");
-    }
-
-    private void runRegisterSimPhoneAccount() throws RemoteException {
-        boolean isEmergencyAccount = false;
-        String opt;
-        while ((opt = nextOption()) != null) {
-            switch (opt) {
-                case "-e": {
-                    isEmergencyAccount = true;
-                    break;
-                }
-            }
-        }
-        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
-        final String label = nextArgRequired();
-        final String address = nextArgRequired();
-        int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER
-                | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
-                | (isEmergencyAccount ? PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS : 0);
-        PhoneAccount account = PhoneAccount.builder(
-            handle, label)
-                .setAddress(Uri.parse(address))
-                .setSubscriptionAddress(Uri.parse(address))
-                .setCapabilities(capabilities)
-                .setShortDescription(label)
-                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
-                .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
-                .build();
-        mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
-        System.out.println("Success - " + handle + " registered.");
-    }
-
-    private void runSetTestCallRedirectionApp() throws RemoteException {
-        final String packageName = nextArg();
-        mTelecomService.setTestDefaultCallRedirectionApp(packageName);
-    }
-
-    private void runSetTestCallScreeningApp() throws RemoteException {
-        final String packageName = nextArg();
-        mTelecomService.setTestDefaultCallScreeningApp(packageName);
-    }
-
-    private void runAddOrRemoveCallCompanionApp() throws RemoteException {
-        final String packageName = nextArgRequired();
-        String isAdded = nextArgRequired();
-        boolean isAddedBool = "1".equals(isAdded);
-        mTelecomService.addOrRemoveTestCallCompanionApp(packageName, isAddedBool);
-    }
-
-    private void runSetCallDiagnosticService() throws RemoteException {
-        String packageName = nextArg();
-        if ("default".equals(packageName)) packageName = null;
-        mTelecomService.setTestCallDiagnosticService(packageName);
-        System.out.println("Success - " + packageName + " set as call diagnostic service.");
-    }
-
-    private void runSetTestPhoneAcctSuggestionComponent() throws RemoteException {
-        final String componentName = nextArg();
-        mTelecomService.setTestPhoneAcctSuggestionComponent(componentName);
-    }
-
-    private void runSetUserSelectedOutgoingPhoneAccount() throws RemoteException {
-        Log.i(this, "runSetUserSelectedOutgoingPhoneAccount");
-        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
-        mTelecomService.setUserSelectedOutgoingPhoneAccount(handle);
-        System.out.println("Success - " + handle + " set as default outgoing account.");
-    }
-
-    private void runUnregisterPhoneAccount() throws RemoteException {
-        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
-        mTelecomService.unregisterPhoneAccount(handle, CALLING_PACKAGE);
-        System.out.println("Success - " + handle + " unregistered.");
-    }
-
-    private void runStopBlockSuppression() throws RemoteException {
-        mTelecomService.stopBlockSuppression();
-    }
-
-    private void runCleanupStuckCalls() throws RemoteException {
-        mTelecomService.cleanupStuckCalls();
-    }
-
-    private void runCleanupOrphanPhoneAccounts() throws RemoteException {
-        System.out.println("Success - cleaned up " + mTelecomService.cleanupOrphanPhoneAccounts()
-                + "  phone accounts.");
-    }
-
-    private void runResetCarMode() throws RemoteException {
-        mTelecomService.resetCarMode();
-    }
-
-    private void runSetDefaultDialer() throws RemoteException {
-        String packageName = nextArg();
-        if ("default".equals(packageName)) packageName = null;
-        mTelecomService.setTestDefaultDialer(packageName);
-        System.out.println("Success - " + packageName + " set as override default dialer.");
-    }
-
-    private void runSetSystemDialer() throws RemoteException {
-        final String flatComponentName = nextArg();
-        final ComponentName componentName = (flatComponentName.equals("default")
-                ? null : parseComponentName(flatComponentName));
-        mTelecomService.setSystemDialer(componentName);
-        System.out.println("Success - " + componentName + " set as override system dialer.");
-    }
-
-    private void runGetDefaultDialer() throws RemoteException {
-        System.out.println(mTelecomService.getDefaultDialerPackage(CALLING_PACKAGE));
-    }
-
-    private void runGetSystemDialer() throws RemoteException {
-        System.out.println(mTelecomService.getSystemDialerPackage(CALLING_PACKAGE));
-    }
-
-    private void runWaitOnHandler() throws RemoteException {
-
-    }
-
-    private void runSetSimCount() throws RemoteException {
-        if (!callerIsRoot()) {
-            System.out.println("set-sim-count requires adb root");
-            return;
-        }
-        int numSims = Integer.parseInt(nextArgRequired());
-        System.out.println("Setting sim count to " + numSims + ". Device may reboot");
-        mTelephonyManager.switchMultiSimConfig(numSims);
-    }
-
-    /**
-     * prints out whether a particular non-ui InCallServices is bound in a call
-     */
-    public void runIsNonUiInCallServiceBound() throws RemoteException {
-        if (TextUtils.isEmpty(mArgs.peekNextArg())) {
-            System.out.println("No Argument passed. Please pass a <PACKAGE_NAME> to lookup.");
-        } else {
-            System.out.println(
-                    String.valueOf(mTelecomService.isNonUiInCallServiceBound(nextArg())));
-        }
-    }
-
-    /**
-     * Prints the mSIM config to the console.
-     * "DSDS" for a phone in DSDS mode
-     * "" (empty string) for a phone in SS mode
-     */
-    private void runGetSimConfig() throws RemoteException {
-        System.out.println(TelephonyProperties.multi_sim_config().orElse(""));
-    }
-
-    private void runGetMaxPhones() throws RemoteException {
-        // how many logical modems can be potentially active simultaneously
-        System.out.println(mTelephonyManager.getSupportedModemCount());
-    }
-
-    private void runSetEmergencyPhoneAccountPackageFilter() throws RemoteException {
-        String packageName = mArgs.getNextArg();
-        if (TextUtils.isEmpty(packageName)) {
-            mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(null);
-            System.out.println("Success - filter cleared");
-        } else {
-            mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(packageName);
-            System.out.println("Success = filter set to " + packageName);
-        }
-
-    }
-
-    private void runLogMark() throws RemoteException {
-        String message = Arrays.stream(mArgs.peekRemainingArgs()).collect(Collectors.joining(" "));
-        mTelecomService.requestLogMark(message);
-    }
-
-    private PhoneAccountHandle getPhoneAccountHandleFromArgs() throws RemoteException {
-        if (TextUtils.isEmpty(mArgs.peekNextArg())) {
-            return null;
-        }
-        final ComponentName component = parseComponentName(nextArgRequired());
-        final String accountId = nextArgRequired();
-        final String userSnInStr = nextArgRequired();
-        UserHandle userHandle;
-        try {
-            final int userSn = Integer.parseInt(userSnInStr);
-            userHandle = UserHandle.of(mUserManager.getUserHandle(userSn));
-        } catch (NumberFormatException ex) {
-            Log.w(this, "getPhoneAccountHandleFromArgs - invalid user %s", userSnInStr);
-            throw new IllegalArgumentException ("Invalid user serial number " + userSnInStr);
-        }
-        return new PhoneAccountHandle(component, accountId, userHandle);
-    }
-
-    private boolean callerIsRoot() {
-        return Process.ROOT_UID == Process.myUid();
-    }
-
-    private ComponentName parseComponentName(String component) {
-        ComponentName cn = ComponentName.unflattenFromString(component);
-        if (cn == null) {
-            throw new IllegalArgumentException ("Invalid component " + component);
-        }
-        return cn;
+        new TelecomShellCommand(service, context).exec(null, FileDescriptor.in,
+                FileDescriptor.out, FileDescriptor.err, args);
     }
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 62980ed..592cbfd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4445,7 +4445,7 @@
     method public final android.media.session.MediaController getMediaController();
     method @NonNull public android.view.MenuInflater getMenuInflater();
     method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher();
-    method public final android.app.Activity getParent();
+    method @Deprecated public final android.app.Activity getParent();
     method @Nullable public android.content.Intent getParentActivityIntent();
     method public android.content.SharedPreferences getPreferences(int);
     method @Nullable public android.net.Uri getReferrer();
@@ -4463,7 +4463,7 @@
     method public void invalidateOptionsMenu();
     method public boolean isActivityTransitionRunning();
     method public boolean isChangingConfigurations();
-    method public final boolean isChild();
+    method @Deprecated public final boolean isChild();
     method public boolean isDestroyed();
     method public boolean isFinishing();
     method public boolean isImmersive();
@@ -5395,7 +5395,7 @@
 
   public final class AutomaticZenRule implements android.os.Parcelable {
     ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean);
-    ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
+    ctor @Deprecated public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
     ctor public AutomaticZenRule(android.os.Parcel);
     method public int describeContents();
     method public android.net.Uri getConditionId();
@@ -6085,7 +6085,7 @@
 
   public class GrammaticalInflectionManager {
     method public int getApplicationGrammaticalGender();
-    method @FlaggedApi("android.app.system_terms_of_address_enabled") public int getSystemGrammaticalGender();
+    method @FlaggedApi("android.app.system_terms_of_address_enabled") @RequiresPermission("android.permission.READ_SYSTEM_GRAMMATICAL_GENDER") public int getSystemGrammaticalGender();
     method public void setRequestedApplicationGrammaticalGender(int);
   }
 
@@ -6674,9 +6674,9 @@
     method @Deprecated public android.app.Notification.Builder addPerson(String);
     method @NonNull public android.app.Notification.Builder addPerson(android.app.Person);
     method @NonNull public android.app.Notification build();
-    method public android.widget.RemoteViews createBigContentView();
-    method public android.widget.RemoteViews createContentView();
-    method public android.widget.RemoteViews createHeadsUpContentView();
+    method @Deprecated public android.widget.RemoteViews createBigContentView();
+    method @Deprecated public android.widget.RemoteViews createContentView();
+    method @Deprecated public android.widget.RemoteViews createHeadsUpContentView();
     method @NonNull public android.app.Notification.Builder extend(android.app.Notification.Extender);
     method public android.os.Bundle getExtras();
     method @Deprecated public android.app.Notification getNotification();
@@ -9608,7 +9608,7 @@
     method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews);
     method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int);
     method public boolean requestPinAppWidget(@NonNull android.content.ComponentName, @Nullable android.os.Bundle, @Nullable android.app.PendingIntent);
-    method @FlaggedApi("android.appwidget.flags.generated_previews") public void setWidgetPreview(@NonNull android.content.ComponentName, int, @NonNull android.widget.RemoteViews);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") public boolean setWidgetPreview(@NonNull android.content.ComponentName, int, @NonNull android.widget.RemoteViews);
     method public void updateAppWidget(int[], android.widget.RemoteViews);
     method public void updateAppWidget(int, android.widget.RemoteViews);
     method public void updateAppWidget(android.content.ComponentName, android.widget.RemoteViews);
@@ -15708,7 +15708,7 @@
     method public boolean clipRect(int, int, int, int);
     method @FlaggedApi("com.android.graphics.hwui.flags.clip_shader") public void clipShader(@NonNull android.graphics.Shader);
     method public void concat(@Nullable android.graphics.Matrix);
-    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void concat44(@Nullable android.graphics.Matrix44);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void concat(@Nullable android.graphics.Matrix44);
     method public void disableZ();
     method public void drawARGB(int, int, int, int);
     method public void drawArc(@NonNull android.graphics.RectF, float, float, boolean, @NonNull android.graphics.Paint);
@@ -16361,7 +16361,7 @@
     ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44();
     ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(@NonNull android.graphics.Matrix);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 concat(@NonNull android.graphics.Matrix44);
-    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(int, int);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(@IntRange(from=0, to=3) int, @IntRange(from=0, to=3) int);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void getValues(@NonNull float[]);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean invert();
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean isIdentity();
@@ -16370,7 +16370,7 @@
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void reset();
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 rotate(float, float, float, float);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 scale(float, float, float);
-    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(int, int, float);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(@IntRange(from=0, to=3) int, @IntRange(from=0, to=3) int, float);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void setValues(@NonNull float[]);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 translate(float, float, float);
   }
@@ -41153,19 +41153,19 @@
     method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications();
     method public final void migrateNotificationFilter(int, @Nullable java.util.List<java.lang.String>);
     method public android.os.IBinder onBind(android.content.Intent);
-    method public void onInterruptionFilterChanged(int);
-    method public void onListenerConnected();
-    method public void onListenerDisconnected();
-    method public void onListenerHintsChanged(int);
-    method public void onNotificationChannelGroupModified(String, android.os.UserHandle, android.app.NotificationChannelGroup, int);
-    method public void onNotificationChannelModified(String, android.os.UserHandle, android.app.NotificationChannel, int);
-    method public void onNotificationPosted(android.service.notification.StatusBarNotification);
-    method public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
-    method public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
-    method public void onNotificationRemoved(android.service.notification.StatusBarNotification);
-    method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
-    method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, int);
-    method public void onSilentStatusBarIconsVisibilityChanged(boolean);
+    method @UiThread public void onInterruptionFilterChanged(int);
+    method @UiThread public void onListenerConnected();
+    method @UiThread public void onListenerDisconnected();
+    method @UiThread public void onListenerHintsChanged(int);
+    method @UiThread public void onNotificationChannelGroupModified(String, android.os.UserHandle, android.app.NotificationChannelGroup, int);
+    method @UiThread public void onNotificationChannelModified(String, android.os.UserHandle, android.app.NotificationChannel, int);
+    method @UiThread public void onNotificationPosted(android.service.notification.StatusBarNotification);
+    method @UiThread public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
+    method @UiThread public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
+    method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification);
+    method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
+    method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, int);
+    method @UiThread public void onSilentStatusBarIconsVisibilityChanged(boolean);
     method public final void requestInterruptionFilter(int);
     method public final void requestListenerHints(int);
     method public static void requestRebind(android.content.ComponentName);
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 9c1a8e8..0ab2588 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -652,6 +652,7 @@
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.webkit.WebViewProviderResponse> CREATOR;
     field public static final int STATUS_FAILED_LISTING_WEBVIEW_PACKAGES = 4; // 0x4
+    field public static final int STATUS_FAILED_OTHER = 11; // 0xb
     field public static final int STATUS_FAILED_WAITING_FOR_RELRO = 3; // 0x3
     field public static final int STATUS_SUCCESS = 0; // 0x0
     field @Nullable public final android.content.pm.PackageInfo packageInfo;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index afb796b..b73f199 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -70,7 +70,7 @@
     field public static final String BIND_NETWORK_RECOMMENDATION_SERVICE = "android.permission.BIND_NETWORK_RECOMMENDATION_SERVICE";
     field public static final String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String BIND_ON_DEVICE_INTELLIGENCE_SERVICE = "android.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE";
-    field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String BIND_ON_DEVICE_TRUSTED_SERVICE = "android.permission.BIND_ON_DEVICE_TRUSTED_SERVICE";
+    field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE = "android.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE";
     field public static final String BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE = "android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE";
     field public static final String BIND_PRINT_RECOMMENDATION_SERVICE = "android.permission.BIND_PRINT_RECOMMENDATION_SERVICE";
     field public static final String BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE = "android.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE";
@@ -403,7 +403,6 @@
     field @Deprecated public static final String UPDATE_TIME_ZONE_RULES = "android.permission.UPDATE_TIME_ZONE_RULES";
     field public static final String UPGRADE_RUNTIME_PERMISSIONS = "android.permission.UPGRADE_RUNTIME_PERMISSIONS";
     field public static final String USER_ACTIVITY = "android.permission.USER_ACTIVITY";
-    field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String USE_BACKGROUND_FACE_AUTHENTICATION = "android.permission.USE_BACKGROUND_FACE_AUTHENTICATION";
     field public static final String USE_COLORIZED_NOTIFICATIONS = "android.permission.USE_COLORIZED_NOTIFICATIONS";
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE";
     field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
@@ -2223,10 +2222,9 @@
   }
 
   public static final class Feature.Builder {
-    ctor public Feature.Builder(int, int, int, @NonNull android.os.PersistableBundle);
+    ctor public Feature.Builder(int);
     method @NonNull public android.app.ondeviceintelligence.Feature build();
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setFeatureParams(@NonNull android.os.PersistableBundle);
-    method @NonNull public android.app.ondeviceintelligence.Feature.Builder setId(int);
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setModelName(@NonNull String);
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setName(@NonNull String);
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setType(int);
@@ -2238,7 +2236,7 @@
     ctor public FeatureDetails(@android.app.ondeviceintelligence.FeatureDetails.Status int);
     method public int describeContents();
     method @NonNull public android.os.PersistableBundle getFeatureDetailParams();
-    method @android.app.ondeviceintelligence.FeatureDetails.Status public int getStatus();
+    method @android.app.ondeviceintelligence.FeatureDetails.Status public int getFeatureStatus();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.FeatureDetails> CREATOR;
     field public static final int FEATURE_STATUS_AVAILABLE = 3; // 0x3
@@ -2251,27 +2249,16 @@
   @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD}) public static @interface FeatureDetails.Status {
   }
 
-  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class FilePart implements android.os.Parcelable {
-    ctor public FilePart(@NonNull String, @NonNull android.os.PersistableBundle, @NonNull String) throws java.io.FileNotFoundException;
-    ctor public FilePart(@NonNull String, @NonNull android.os.PersistableBundle, @NonNull java.io.FileInputStream) throws java.io.IOException;
-    method public int describeContents();
-    method @NonNull public java.io.FileInputStream getFileInputStream();
-    method @NonNull public String getFilePartKey();
-    method @NonNull public android.os.PersistableBundle getFilePartParams();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.FilePart> CREATOR;
-  }
-
   @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public class OnDeviceIntelligenceManager {
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeature(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeatureDetails(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method @Nullable @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName();
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getVersion(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.LongConsumer);
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void listFeatures(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
-    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequest(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
-    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.StreamingResponseReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
+    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequest(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.ProcessingOutcomeReceiver);
+    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver);
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestFeatureDownload(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.DownloadCallback);
-    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestTokenCount(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Long,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
-    field public static final String API_VERSION_BUNDLE_KEY = "ApiVersionBundleKey";
+    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestTokenInfo(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     field public static final int REQUEST_TYPE_EMBEDDINGS = 2; // 0x2
     field public static final int REQUEST_TYPE_INFERENCE = 0; // 0x0
     field public static final int REQUEST_TYPE_PREPARE = 1; // 0x1
@@ -2305,6 +2292,10 @@
     field public static final int PROCESSING_ERROR_UNKNOWN = 1; // 0x1
   }
 
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface ProcessingOutcomeReceiver extends android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> {
+    method public default void onDataAugmentRequest(@NonNull android.app.ondeviceintelligence.Content, @NonNull java.util.function.Consumer<android.app.ondeviceintelligence.Content>);
+  }
+
   @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class ProcessingSignal {
     ctor public ProcessingSignal();
     method public void sendSignal(@NonNull android.os.PersistableBundle);
@@ -2315,8 +2306,18 @@
     method public void onSignalReceived(@NonNull android.os.PersistableBundle);
   }
 
-  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface StreamingResponseReceiver<R, T, E extends java.lang.Throwable> extends android.os.OutcomeReceiver<R,E> {
-    method public void onNewContent(@NonNull T);
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface StreamedProcessingOutcomeReceiver extends android.app.ondeviceintelligence.ProcessingOutcomeReceiver {
+    method public void onNewContent(@NonNull android.app.ondeviceintelligence.Content);
+  }
+
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class TokenInfo implements android.os.Parcelable {
+    ctor public TokenInfo(long, @NonNull android.os.PersistableBundle);
+    ctor public TokenInfo(long);
+    method public int describeContents();
+    method public long getCount();
+    method @NonNull public android.os.PersistableBundle getInfoParams();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.TokenInfo> CREATOR;
   }
 
 }
@@ -3759,7 +3760,6 @@
     field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation";
     field public static final String ETHERNET_SERVICE = "ethernet";
     field public static final String EUICC_CARD_SERVICE = "euicc_card";
-    field @FlaggedApi("android.hardware.biometrics.face_background_authentication") public static final String FACE_SERVICE = "face";
     field public static final String FONT_SERVICE = "font";
     field public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
     field public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding";
@@ -4386,7 +4386,7 @@
     field public static final int PROTECTION_FLAG_MODULE = 4194304; // 0x400000
     field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000
     field public static final int PROTECTION_FLAG_RECENTS = 33554432; // 0x2000000
-    field public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000
+    field @Deprecated public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000
     field public static final int PROTECTION_FLAG_ROLE = 67108864; // 0x4000000
     field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000
     field public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 32768; // 0x8000
@@ -4821,7 +4821,7 @@
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean areAnySensorPrivacyTogglesEnabled(int);
-    method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @NonNull @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public java.util.Map<java.lang.String,java.lang.Boolean> getCameraPrivacyAllowlist();
+    method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @NonNull @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public java.util.List<java.lang.String> getCameraPrivacyAllowlist();
     method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public int getSensorPrivacyState(int, int);
     method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isCameraPrivacyEnabled(@NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isSensorPrivacyEnabled(int);
@@ -4844,6 +4844,12 @@
     method public boolean isEnabled();
   }
 
+  @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") public static class SensorPrivacyManager.StateTypes {
+    field public static final int DISABLED = 2; // 0x2
+    field public static final int ENABLED = 1; // 0x1
+    field public static final int ENABLED_EXCEPT_ALLOWLISTED_APPS = 3; // 0x3
+  }
+
 }
 
 package android.hardware.biometrics {
@@ -5131,15 +5137,6 @@
 
 }
 
-package android.hardware.face {
-
-  @FlaggedApi("android.hardware.biometrics.face_background_authentication") public class FaceManager {
-    method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION) public void authenticateInBackground(@Nullable java.util.concurrent.Executor, @Nullable android.hardware.biometrics.BiometricPrompt.CryptoObject, @Nullable android.os.CancellationSignal, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
-    method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @RequiresPermission(anyOf={"android.permission.USE_BIOMETRIC_INTERNAL", android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION}) public boolean hasEnrolledTemplates();
-  }
-
-}
-
 package android.hardware.hdmi {
 
   public abstract class HdmiClient {
@@ -11531,7 +11528,7 @@
     method public int checkPermissionForPreflight(@NonNull String, @NonNull android.content.AttributionSource);
     method @RequiresPermission(value=android.Manifest.permission.UPDATE_APP_OPS_STATS, conditional=true) public int checkPermissionForStartDataDelivery(@NonNull String, @NonNull android.content.AttributionSource, @Nullable String);
     method public void finishDataDelivery(@NonNull String, @NonNull android.content.AttributionSource);
-    method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") @NonNull @RequiresPermission(android.Manifest.permission.GET_RUNTIME_PERMISSIONS) public java.util.Map<java.lang.String,android.permission.PermissionManager.PermissionState> getAllPermissionStates(@NonNull String, @NonNull String);
+    method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, android.Manifest.permission.GET_RUNTIME_PERMISSIONS}) public java.util.Map<java.lang.String,android.permission.PermissionManager.PermissionState> getAllPermissionStates(@NonNull String, @NonNull String);
     method @NonNull @RequiresPermission(android.Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY) public java.util.Set<java.lang.String> getAutoRevokeExemptionGrantedPackages();
     method @NonNull @RequiresPermission(android.Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY) public java.util.Set<java.lang.String> getAutoRevokeExemptionRequestedPackages();
     method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, android.Manifest.permission.GET_RUNTIME_PERMISSIONS}) public int getPermissionFlags(@NonNull String, @NonNull String, @NonNull String);
@@ -12885,7 +12882,7 @@
   }
 
   public abstract class NotificationListenerService extends android.app.Service {
-    method public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int);
+    method @UiThread public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int);
   }
 
   public static class NotificationListenerService.Ranking {
@@ -12957,17 +12954,19 @@
   @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceIntelligenceService extends android.app.Service {
     ctor public OnDeviceIntelligenceService();
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
-    method public abstract void onDownloadFeature(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull android.app.ondeviceintelligence.DownloadCallback);
-    method public abstract void onGetFeature(int, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
-    method public abstract void onGetFeatureDetails(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method public abstract void onDownloadFeature(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull android.app.ondeviceintelligence.DownloadCallback);
+    method public abstract void onGetFeature(int, int, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method public abstract void onGetFeatureDetails(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     method public abstract void onGetReadOnlyFeatureFileDescriptorMap(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,android.os.ParcelFileDescriptor>>);
     method public abstract void onGetVersion(@NonNull java.util.function.LongConsumer);
-    method public abstract void onListFeatures(@NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method public abstract void onInferenceServiceConnected();
+    method public abstract void onInferenceServiceDisconnected();
+    method public abstract void onListFeatures(int, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     method public final void updateProcessingState(@NonNull android.os.Bundle, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException>);
     field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceIntelligenceService";
   }
 
-  public static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception {
+  public abstract static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception {
     ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int);
     ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int, @NonNull String);
     method public int getErrorCode();
@@ -12979,17 +12978,18 @@
     field public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 1; // 0x1
   }
 
-  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceTrustedInferenceService extends android.app.Service {
-    ctor public OnDeviceTrustedInferenceService();
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceSandboxedInferenceService extends android.app.Service {
+    ctor public OnDeviceSandboxedInferenceService();
     method public final void fetchFeatureFileInputStreamMap(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,java.io.FileInputStream>>);
+    method @NonNull public java.util.concurrent.Executor getCallbackExecutor();
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
-    method @NonNull public abstract void onCountTokens(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<java.lang.Long,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
-    method @NonNull public abstract void onProcessRequest(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
-    method @NonNull public abstract void onProcessRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.StreamingResponseReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
+    method @NonNull public abstract void onProcessRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.ProcessingOutcomeReceiver);
+    method @NonNull public abstract void onProcessRequestStreaming(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver);
+    method @NonNull public abstract void onTokenInfoRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
     method public abstract void onUpdateProcessingState(@NonNull android.os.Bundle, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException>);
     method public final java.io.FileInputStream openFileInput(@NonNull String) throws java.io.FileNotFoundException;
     method public final void openFileInputAsync(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.io.FileInputStream>) throws java.io.FileNotFoundException;
-    field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceTrustedInferenceService";
+    field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService";
   }
 
 }
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 1923641..1e72a06 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -511,9 +511,9 @@
 
 InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceIntelligenceService#onBind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#onBind(android.content.Intent) parameter #0:
+InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService#onBind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#openFileInput(String) parameter #0:
+InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService#openFileInput(String) parameter #0:
     Invalid nullability on parameter `filename` in method `openFileInput`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: android.service.textclassifier.TextClassifierService#onUnbind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onUnbind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
@@ -571,7 +571,7 @@
     Missing nullability on parameter `args` in method `dump`
 MissingNullability: android.service.notification.NotificationAssistantService#attachBaseContext(android.content.Context) parameter #0:
     Missing nullability on parameter `base` in method `attachBaseContext`
-MissingNullability: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#openFileInput(String):
+MissingNullability: android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService#openFileInput(String):
     Missing nullability on method `openFileInput` return
 MissingNullability: android.telephony.NetworkService#onUnbind(android.content.Intent) parameter #0:
     Missing nullability on parameter `intent` in method `onUnbind`
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 17ed908..53d0c03 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1519,6 +1519,7 @@
 package android.hardware {
 
   public final class SensorPrivacyManager {
+    method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setCameraPrivacyAllowlist(@NonNull java.util.List<java.lang.String>);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, int, boolean);
     method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacyState(int, int, int);
   }
@@ -1547,10 +1548,6 @@
     method public boolean isAllowBackgroundAuthentication();
   }
 
-  public abstract static class BiometricPrompt.AuthenticationCallback {
-    method @FlaggedApi("android.hardware.biometrics.face_background_authentication") public void onAuthenticationAcquired(int);
-  }
-
   public static class BiometricPrompt.Builder {
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.TEST_BIOMETRIC, "android.permission.USE_BIOMETRIC_INTERNAL"}) public android.hardware.biometrics.BiometricPrompt.Builder setAllowBackgroundAuthentication(boolean);
     method @FlaggedApi("android.multiuser.enable_biometrics_to_unlock_private_space") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.TEST_BIOMETRIC, "android.permission.USE_BIOMETRIC_INTERNAL"}) public android.hardware.biometrics.BiometricPrompt.Builder setAllowBackgroundAuthentication(boolean, boolean);
@@ -1569,7 +1566,6 @@
   }
 
   public class SensorProperties {
-    ctor @FlaggedApi("android.hardware.biometrics.face_background_authentication") public SensorProperties(int, int, @NonNull java.util.List<android.hardware.biometrics.SensorProperties.ComponentInfo>);
     method @NonNull public java.util.List<android.hardware.biometrics.SensorProperties.ComponentInfo> getComponentInfo();
     method public int getSensorId();
     method public int getSensorStrength();
@@ -1714,18 +1710,6 @@
 
 }
 
-package android.hardware.face {
-
-  @FlaggedApi("android.hardware.biometrics.face_background_authentication") public class FaceManager {
-    method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
-    method @FlaggedApi("android.hardware.biometrics.face_background_authentication") @NonNull public java.util.List<android.hardware.face.FaceSensorProperties> getSensorProperties();
-  }
-
-  @FlaggedApi("android.hardware.biometrics.face_background_authentication") public class FaceSensorProperties extends android.hardware.biometrics.SensorProperties {
-  }
-
-}
-
 package android.hardware.fingerprint {
 
   @Deprecated public class FingerprintManager {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 63cafdc..44dc8e2 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1254,12 +1254,23 @@
         return mApplication;
     }
 
-    /** Is this activity embedded inside of another activity? */
+    /**
+     * Whether this is a child {@link Activity} of an {@link ActivityGroup}.
+     *
+     * @deprecated {@link ActivityGroup} is deprecated.
+     */
+    @Deprecated
     public final boolean isChild() {
         return mParent != null;
     }
 
-    /** Return the parent activity if this view is an embedded child. */
+    /**
+     * Returns the parent {@link Activity} if this is a child {@link Activity} of an
+     * {@link ActivityGroup}.
+     *
+     * @deprecated {@link ActivityGroup} is deprecated.
+     */
+    @Deprecated
     public final Activity getParent() {
         return mParent;
     }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index a80d70e..1d39186 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4565,7 +4565,7 @@
     private void schedulePauseWithUserLeavingHint(ActivityClientRecord r) {
         final ClientTransaction transaction = ClientTransaction.obtain(mAppThread);
         final PauseActivityItem pauseActivityItem = PauseActivityItem.obtain(r.token,
-                r.activity.isFinishing(), /* userLeaving */ true, r.activity.mConfigChangeFlags,
+                r.activity.isFinishing(), /* userLeaving */ true,
                 /* dontReport */ false, /* autoEnteringPip */ false);
         transaction.addTransactionItem(pauseActivityItem);
         executeTransaction(transaction);
@@ -5439,13 +5439,12 @@
 
     @Override
     public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
-            int configChanges, boolean autoEnteringPip, PendingTransactionActions pendingActions,
+            boolean autoEnteringPip, PendingTransactionActions pendingActions,
             String reason) {
         if (userLeaving) {
             performUserLeavingActivity(r);
         }
 
-        r.activity.mConfigChangeFlags |= configChanges;
         if (autoEnteringPip) {
             // Set mIsInPictureInPictureMode earlier in case of auto-enter-pip, see also
             // {@link Activity#enterPictureInPictureMode(PictureInPictureParams)}.
@@ -5694,9 +5693,8 @@
     }
 
     @Override
-    public void handleStopActivity(ActivityClientRecord r, int configChanges,
+    public void handleStopActivity(ActivityClientRecord r,
             PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
-        r.activity.mConfigChangeFlags |= configChanges;
 
         final StopInfo stopInfo = new StopInfo();
         performStopActivityInner(r, stopInfo, true /* saveState */, finalStateRequest,
@@ -5866,11 +5864,10 @@
 
     /** Core implementation of activity destroy call. */
     void performDestroyActivity(ActivityClientRecord r, boolean finishing,
-            int configChanges, boolean getNonConfigInstance, String reason) {
+            boolean getNonConfigInstance, String reason) {
         Class<? extends Activity> activityClass;
         if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
         activityClass = r.activity.getClass();
-        r.activity.mConfigChangeFlags |= configChanges;
         if (finishing) {
             r.activity.mFinished = true;
         }
@@ -5935,9 +5932,9 @@
     }
 
     @Override
-    public void handleDestroyActivity(ActivityClientRecord r, boolean finishing, int configChanges,
+    public void handleDestroyActivity(ActivityClientRecord r, boolean finishing,
             boolean getNonConfigInstance, String reason) {
-        performDestroyActivity(r, finishing, configChanges, getNonConfigInstance, reason);
+        performDestroyActivity(r, finishing, getNonConfigInstance, reason);
         cleanUpPendingRemoveWindows(r, finishing);
         WindowManager wm = r.activity.getWindowManager();
         View v = r.activity.mDecor;
@@ -6137,7 +6134,7 @@
 
         r.activity.mChangingConfigurations = true;
 
-        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
+        handleRelaunchActivityInner(r, tmp.pendingResults, tmp.pendingIntents,
                 pendingActions, tmp.startsNotResumed, tmp.overrideConfig, tmp.mActivityWindowInfo,
                 "handleRelaunchActivity");
     }
@@ -6206,7 +6203,7 @@
         executeTransaction(transaction);
     }
 
-    private void handleRelaunchActivityInner(@NonNull ActivityClientRecord r, int configChanges,
+    private void handleRelaunchActivityInner(@NonNull ActivityClientRecord r,
             @Nullable List<ResultInfo> pendingResults,
             @Nullable List<ReferrerIntent> pendingIntents,
             @NonNull PendingTransactionActions pendingActions, boolean startsNotResumed,
@@ -6222,7 +6219,7 @@
             callActivityOnStop(r, true /* saveState */, reason);
         }
 
-        handleDestroyActivity(r, false, configChanges, true, reason);
+        handleDestroyActivity(r, false /* finishing */, true /* getNonConfigInstance */, reason);
 
         r.activity = null;
         r.window = null;
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index f6ec370..5e2397d 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -173,8 +173,8 @@
      *                           interrupt the user (e.g. via sound &amp; vibration) while this rule
      *                           is active.
      * @param enabled Whether the rule is enabled.
-     * @deprecated use {@link #AutomaticZenRule(String, ComponentName, ComponentName, Uri,
-     * ZenPolicy, int, boolean)}.
+     *
+     * @deprecated Use {@link AutomaticZenRule.Builder} to construct an {@link AutomaticZenRule}.
      */
     @Deprecated
     public AutomaticZenRule(String name, ComponentName owner, Uri conditionId,
@@ -206,8 +206,10 @@
      *               while this rule is active. This overrides the global policy while this rule is
      *               action ({@link Condition#STATE_TRUE}).
      * @param enabled Whether the rule is enabled.
+     *
+     * @deprecated Use {@link AutomaticZenRule.Builder} to construct an {@link AutomaticZenRule}.
      */
-    // TODO (b/309088420): deprecate this constructor in favor of the builder
+    @Deprecated
     public AutomaticZenRule(@NonNull String name, @Nullable ComponentName owner,
             @Nullable ComponentName configurationActivity, @NonNull Uri conditionId,
             @Nullable ZenPolicy policy, int interruptionFilter, boolean enabled) {
@@ -368,6 +370,9 @@
 
     /**
      * Sets the zen policy.
+     *
+     * <p>When updating an existing rule via {@link NotificationManager#updateAutomaticZenRule},
+     * a {@code null} value here means the previous policy is retained.
      */
     public void setZenPolicy(@Nullable ZenPolicy zenPolicy) {
         this.mZenPolicy = (zenPolicy == null ? null : zenPolicy.copy());
@@ -390,7 +395,12 @@
      * Sets the configuration activity - an activity that handles
      * {@link NotificationManager#ACTION_AUTOMATIC_ZEN_RULE} that shows the user more information
      * about this rule and/or allows them to configure it. This is required to be non-null for rules
-     * that are not backed by {@link android.service.notification.ConditionProviderService}.
+     * that are not backed by a {@link android.service.notification.ConditionProviderService}.
+     *
+     * <p>This is exclusive with the {@code owner} supplied in the constructor; rules where a
+     * configuration activity is set will not use the
+     * {@link android.service.notification.ConditionProviderService} supplied there to determine
+     * whether the rule should be active.
      */
     public void setConfigurationActivity(@Nullable ComponentName componentName) {
         this.configurationActivity = getTrimmedComponentName(componentName);
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index b5b3669..01153c9 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -114,11 +114,11 @@
 
     /** Destroy the activity. */
     public abstract void handleDestroyActivity(@NonNull ActivityClientRecord r, boolean finishing,
-            int configChanges, boolean getNonConfigInstance, String reason);
+            boolean getNonConfigInstance, String reason);
 
     /** Pause the activity. */
     public abstract void handlePauseActivity(@NonNull ActivityClientRecord r, boolean finished,
-            boolean userLeaving, int configChanges, boolean autoEnteringPip,
+            boolean userLeaving, boolean autoEnteringPip,
             PendingTransactionActions pendingActions, String reason);
 
     /**
@@ -146,14 +146,13 @@
     /**
      * Stop the activity.
      * @param r Target activity record.
-     * @param configChanges Activity configuration changes.
      * @param pendingActions Pending actions to be used on this or later stages of activity
      *                       transaction.
      * @param finalStateRequest Flag indicating if this call is handling final lifecycle state
      *                          request for a transaction.
      * @param reason Reason for performing this operation.
      */
-    public abstract void handleStopActivity(@NonNull ActivityClientRecord r, int configChanges,
+    public abstract void handleStopActivity(@NonNull ActivityClientRecord r,
             PendingTransactionActions pendingActions, boolean finalStateRequest, String reason);
 
     /** Report that activity was stopped to server. */
diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java
index 483a6e1..4ce983f 100644
--- a/core/java/android/app/GrammaticalInflectionManager.java
+++ b/core/java/android/app/GrammaticalInflectionManager.java
@@ -16,8 +16,10 @@
 
 package android.app;
 
+import android.Manifest;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemService;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -127,6 +129,7 @@
      *
      * @see Configuration#getGrammaticalGender
      */
+    @RequiresPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER)
     @FlaggedApi(Flags.FLAG_SYSTEM_TERMS_OF_ADDRESS_ENABLED)
     @Configuration.GrammaticalGender
     public int getSystemGrammaticalGender() {
diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java
index 1b19ecd..095cfc5 100644
--- a/core/java/android/app/LocalActivityManager.java
+++ b/core/java/android/app/LocalActivityManager.java
@@ -413,7 +413,7 @@
         if (localLOGV) Log.v(TAG, r.id + ": destroying");
         final ActivityClientRecord clientRecord = mActivityThread.getActivityClient(r);
         if (clientRecord != null) {
-            mActivityThread.performDestroyActivity(clientRecord, finish, 0 /* configChanges */,
+            mActivityThread.performDestroyActivity(clientRecord, finish,
                     false /* getNonConfigInstance */, "LocalActivityManager::performDestroy");
         }
         r.activity = null;
@@ -684,7 +684,7 @@
                 if (localLOGV) Log.v(TAG, r.id + ": no corresponding record");
                 continue;
             }
-            mActivityThread.performDestroyActivity(clientRecord, finishing, 0 /* configChanges */,
+            mActivityThread.performDestroyActivity(clientRecord, finishing,
                     false /* getNonConfigInstance */, "LocalActivityManager::dispatchDestroy");
         }
         mActivities.clear();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 1129f9d..79cb09d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6068,11 +6068,19 @@
         }
 
         /**
-         * Construct a RemoteViews for the final 1U notification layout. In order:
-         *   1. Custom contentView from the caller
-         *   2. Style's proposed content view
-         *   3. Standard template view
+         * Construct a RemoteViews representing the standard notification layout.
+         *
+         * @deprecated For performance and system health reasons, this API is no longer required to
+         *  be used directly by the System UI when rendering Notifications to the user. While the UI
+         *  returned by this method will still represent the content of the Notification being
+         *  built, it may differ from the visual style of the system.
+         *
+         *  NOTE: this API has always had severe limitations; for example it does not support any
+         *  interactivity, it ignores the app theme, it hard-codes the colors from the system theme
+         *  at the time it is called, and it does Bitmap decoding on the main thread which can cause
+         *  UI jank.
          */
+        @Deprecated
         public RemoteViews createContentView() {
             return createContentView(false /* increasedheight */ );
         }
@@ -6181,8 +6189,19 @@
         }
 
         /**
-         * Construct a RemoteViews for the final big notification layout.
+         * Construct a RemoteViews representing the expanded notification layout.
+         *
+         * @deprecated For performance and system health reasons, this API is no longer required to
+         *  be used directly by the System UI when rendering Notifications to the user. While the UI
+         *  returned by this method will still represent the content of the Notification being
+         *  built, it may differ from the visual style of the system.
+         *
+         *  NOTE: this API has always had severe limitations; for example it does not support any
+         *  interactivity, it ignores the app theme, it hard-codes the colors from the system theme
+         *  at the time it is called, and it does Bitmap decoding on the main thread which can cause
+         *  UI jank.
          */
+        @Deprecated
         public RemoteViews createBigContentView() {
             RemoteViews result = null;
             if (useExistingRemoteView(mN.bigContentView)) {
@@ -6315,8 +6334,19 @@
         }
 
         /**
-         * Construct a RemoteViews for the final heads-up notification layout.
+         * Construct a RemoteViews representing the heads up notification layout.
+         *
+         * @deprecated For performance and system health reasons, this API is no longer required to
+         *  be used directly by the System UI when rendering Notifications to the user. While the UI
+         *  returned by this method will still represent the content of the Notification being
+         *  built, it may differ from the visual style of the system.
+         *
+         *  NOTE: this API has always had severe limitations; for example it does not support any
+         *  interactivity, it ignores the app theme, it hard-codes the colors from the system theme
+         *  at the time it is called, and it does Bitmap decoding on the main thread which can cause
+         *  UI jank.
          */
+        @Deprecated
         public RemoteViews createHeadsUpContentView() {
             return createHeadsUpContentView(false /* useIncreasedHeight */);
         }
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index d49a254..b82a1e3 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -270,13 +270,16 @@
      * Integer extra for {@link #ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED} containing the state of
      * the {@link AutomaticZenRule}.
      *
-     * <p>
-     *     The value will be one of {@link #AUTOMATIC_RULE_STATUS_ENABLED},
-     *     {@link #AUTOMATIC_RULE_STATUS_DISABLED}, {@link #AUTOMATIC_RULE_STATUS_REMOVED},
-     *     {@link #AUTOMATIC_RULE_STATUS_UNKNOWN}.
-     * </p>
+     * <p>The value will be one of {@link #AUTOMATIC_RULE_STATUS_ENABLED},
+     * {@link #AUTOMATIC_RULE_STATUS_DISABLED}, {@link #AUTOMATIC_RULE_STATUS_REMOVED},
+     * {@link #AUTOMATIC_RULE_STATUS_ACTIVATED}, {@link #AUTOMATIC_RULE_STATUS_DEACTIVATED}, or
+     * {@link #AUTOMATIC_RULE_STATUS_UNKNOWN}.
+     *
+     * <p>Note that the {@link #AUTOMATIC_RULE_STATUS_ACTIVATED} and
+     * {@link #AUTOMATIC_RULE_STATUS_DEACTIVATED} statuses are only sent to packages targeting
+     * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above; apps targeting a lower SDK version
+     * will be sent {@link #AUTOMATIC_RULE_STATUS_UNKNOWN} in their place instead.
      */
-    // TODO (b/309101513): Add new status types to javadoc
     public static final String EXTRA_AUTOMATIC_ZEN_RULE_STATUS =
             "android.app.extra.AUTOMATIC_ZEN_RULE_STATUS";
 
@@ -370,11 +373,15 @@
             = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED";
 
     /**
-     * Intent that is broadcast when the state of getNotificationPolicy() changes.
+     * Intent that is broadcast when the state of {@link #getNotificationPolicy()} changes.
      *
      * <p>This broadcast is only sent to registered receivers and (starting from
      * {@link Build.VERSION_CODES#Q}) receivers in packages that have been granted Do Not
      * Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
+     *
+     * <p>Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, most calls to
+     * {@link #setNotificationPolicy(Policy)} will update the app's implicit rule policy instead of
+     * the global policy, so this broadcast will be sent much less frequently.
      */
     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_NOTIFICATION_POLICY_CHANGED
@@ -1378,12 +1385,16 @@
     /**
      * Updates the given zen rule.
      *
-     * <p>
-     * Throws a SecurityException if policy access is not granted to this package.
+     * <p>Before {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, updating a rule that is not backed
+     * up by a {@link android.service.notification.ConditionProviderService} will deactivate it if
+     * it was previously active. Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this
+     * will only happen if the rule's definition is actually changing.
+     *
+     * <p>Throws a SecurityException if policy access is not granted to this package.
      * See {@link #isNotificationPolicyAccessGranted}.
      *
-     * <p>
-     * Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}.
+     * <p>Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}.
+     *
      * @param id The id of the rule to update
      * @param automaticZenRule the rule to update.
      * @return Whether the rule was successfully updated.
@@ -1744,9 +1755,11 @@
     /**
      * Gets the current user-specified default notification policy.
      *
-     * <p>
+     * <p>For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) this method will return the policy associated
+     * to their implicit {@link AutomaticZenRule} instead, if it exists. See
+     * {@link #setNotificationPolicy(Policy)}.
      */
-    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public Policy getNotificationPolicy() {
         INotificationManager service = getService();
         try {
@@ -1757,15 +1770,20 @@
     }
 
     /**
-     * Sets the current notification policy.
+     * Sets the current notification policy (which applies when {@link #setInterruptionFilter} is
+     * called with the {@link #INTERRUPTION_FILTER_PRIORITY} value).
      *
-     * <p>
-     * Only available if policy access is granted to this package.
-     * See {@link #isNotificationPolicyAccessGranted}.
+     * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) cannot modify the global notification policy.
+     * Calling this method will instead create or update an {@link AutomaticZenRule} associated to
+     * the app, using a {@link ZenPolicy} corresponding to the {@link Policy} supplied here, and
+     * which will be activated/deactivated by calls to {@link #setInterruptionFilter(int)}.
+     *
+     * <p>Only available if policy access is granted to this package. See
+     * {@link #isNotificationPolicyAccessGranted}.
      *
      * @param policy The new desired policy.
      */
-    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
         setNotificationPolicy(policy, /* fromUser= */ false);
     }
@@ -2052,10 +2070,12 @@
 
         /** Notification senders to prioritize for calls. One of:
          * PRIORITY_SENDERS_ANY, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED */
+        @PrioritySenders
         public final int priorityCallSenders;
 
         /** Notification senders to prioritize for messages. One of:
          * PRIORITY_SENDERS_ANY, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED */
+        @PrioritySenders
         public final int priorityMessageSenders;
 
         /**
@@ -2063,6 +2083,7 @@
          * {@link #CONVERSATION_SENDERS_NONE}, {@link #CONVERSATION_SENDERS_IMPORTANT},
          * {@link #CONVERSATION_SENDERS_ANYONE}.
          */
+        @ConversationSenders
         public final int priorityConversationSenders;
 
         /**
@@ -2630,16 +2651,19 @@
         }
 
         /** @hide **/
+        @PrioritySenders
         public int allowCallsFrom() {
             return priorityCallSenders;
         }
 
         /** @hide **/
+        @PrioritySenders
         public int allowMessagesFrom() {
             return priorityMessageSenders;
         }
 
         /** @hide **/
+        @ConversationSenders
         public int allowConversationsFrom() {
             return priorityConversationSenders;
         }
@@ -2780,11 +2804,17 @@
      * The interruption filter defines which notifications are allowed to
      * interrupt the user (e.g. via sound &amp; vibration) and is applied
      * globally.
-     * <p>
-     * Only available if policy access is granted to this package. See
+     *
+     * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) cannot modify the global interruption filter.
+     * Calling this method will instead activate or deactivate an {@link AutomaticZenRule}
+     * associated to the app, using a {@link ZenPolicy} that corresponds to the {@link Policy}
+     * supplied to {@link #setNotificationPolicy(Policy)} (or the global policy when one wasn't
+     * provided).
+     *
+     * <p> Only available if policy access is granted to this package. See
      * {@link #isNotificationPolicyAccessGranted}.
      */
-    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
         setInterruptionFilter(interruptionFilter, /* fromUser= */ false);
     }
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 6255260..8b84f06 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -124,6 +124,32 @@
      */
     private LocaleConfig mLocaleConfig = new LocaleConfig(LocaleList.getEmptyLocaleList());
 
+    private final ArrayMap<String, SharedLibraryAssets> mSharedLibAssetsMap =
+            new ArrayMap<>();
+
+    /**
+     * The internal function to register the resources paths of a package (e.g. a shared library).
+     * This will collect the package resources' paths from its ApplicationInfo and add them to all
+     * existing and future contexts while the application is running.
+     */
+    public void registerResourcePaths(@NonNull String uniqueId, @NonNull ApplicationInfo appInfo) {
+        SharedLibraryAssets sharedLibAssets = new SharedLibraryAssets(appInfo.sourceDir,
+                appInfo.splitSourceDirs, appInfo.sharedLibraryFiles,
+                appInfo.resourceDirs, appInfo.overlayPaths);
+
+        synchronized (mLock) {
+            if (mSharedLibAssetsMap.containsKey(uniqueId)) {
+                Slog.v(TAG, "Package resources' paths for uniqueId: " + uniqueId
+                        + " has already been registered, this is a no-op.");
+                return;
+            }
+            mSharedLibAssetsMap.put(uniqueId, sharedLibAssets);
+            appendLibAssetsLocked(sharedLibAssets.getAllAssetPaths());
+            Slog.v(TAG, "The following resources' paths have been added: "
+                    + Arrays.toString(sharedLibAssets.getAllAssetPaths()));
+        }
+    }
+
     private static class ApkKey {
         public final String path;
         public final boolean sharedLib;
@@ -278,6 +304,21 @@
     public ResourcesManager() {
     }
 
+    /**
+     * Inject a customized ResourcesManager instance for testing, return the old ResourcesManager
+     * instance.
+     */
+    @UnsupportedAppUsage
+    @VisibleForTesting
+    public static ResourcesManager setInstance(ResourcesManager resourcesManager) {
+        synchronized (ResourcesManager.class) {
+            ResourcesManager oldResourceManager = sResourcesManager;
+            sResourcesManager = resourcesManager;
+            return oldResourceManager;
+        }
+
+    }
+
     @UnsupportedAppUsage
     public static ResourcesManager getInstance() {
         synchronized (ResourcesManager.class) {
@@ -1480,6 +1521,56 @@
         }
     }
 
+    private void appendLibAssetsLocked(String[] libAssets) {
+        synchronized (mLock) {
+            // Record which ResourcesImpl need updating
+            // (and what ResourcesKey they should update to).
+            final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();
+
+            final int implCount = mResourceImpls.size();
+            for (int i = 0; i < implCount; i++) {
+                final ResourcesKey key = mResourceImpls.keyAt(i);
+                final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
+                final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
+                if (impl == null) {
+                    Slog.w(TAG, "Found a ResourcesImpl which is null, skip it and continue to "
+                            + "append shared library assets for next ResourcesImpl.");
+                    continue;
+                }
+
+                var newDirs = new ArrayList<String>();
+                var dirsSet = new ArraySet<String>();
+                if (key.mLibDirs != null) {
+                    final int dirsLength = key.mLibDirs.length;
+                    for (int k = 0; k < dirsLength; k++) {
+                        newDirs.add(key.mLibDirs[k]);
+                        dirsSet.add(key.mLibDirs[k]);
+                    }
+                }
+                final int assetsLength = libAssets.length;
+                for (int j = 0; j < assetsLength; j++) {
+                    if (dirsSet.add(libAssets[j])) {
+                        newDirs.add(libAssets[j]);
+                    }
+                }
+                String[] newLibAssets = newDirs.toArray(new String[0]);
+                if (!Arrays.equals(newLibAssets, key.mLibDirs)) {
+                    updatedResourceKeys.put(impl, new ResourcesKey(
+                            key.mResDir,
+                            key.mSplitResDirs,
+                            key.mOverlayPaths,
+                            newLibAssets,
+                            key.mDisplayId,
+                            key.mOverrideConfiguration,
+                            key.mCompatInfo,
+                            key.mLoaders));
+                }
+            }
+
+            redirectResourcesToNewImplLocked(updatedResourceKeys);
+        }
+    }
+
     private void applyNewResourceDirsLocked(@Nullable final String[] oldSourceDirs,
             @NonNull final ApplicationInfo appInfo) {
         try {
@@ -1689,4 +1780,50 @@
             }
         }
     }
+
+    public static class SharedLibraryAssets{
+        private final String[] mAssetPaths;
+
+        SharedLibraryAssets(String sourceDir, String[] splitSourceDirs, String[] sharedLibraryFiles,
+                String[] resourceDirs, String[] overlayPaths) {
+            mAssetPaths = collectAssetPaths(sourceDir, splitSourceDirs, sharedLibraryFiles,
+                    resourceDirs, overlayPaths);
+        }
+
+        private @NonNull String[] collectAssetPaths(String sourceDir, String[] splitSourceDirs,
+                String[] sharedLibraryFiles, String[] resourceDirs, String[] overlayPaths) {
+            final String[][] inputLists = {
+                    splitSourceDirs, sharedLibraryFiles, resourceDirs, overlayPaths
+            };
+
+            final ArraySet<String> assetPathSet = new ArraySet<>();
+            final List<String> assetPathList = new ArrayList<>();
+            if (sourceDir != null) {
+                assetPathSet.add(sourceDir);
+                assetPathList.add(sourceDir);
+            }
+
+            for (int i = 0; i < inputLists.length; i++) {
+                if (inputLists[i] != null) {
+                    for (int j = 0; j < inputLists[i].length; j++) {
+                        if (assetPathSet.add(inputLists[i][j])) {
+                            assetPathList.add(inputLists[i][j]);
+                        }
+                    }
+                }
+            }
+            return assetPathList.toArray(new String[0]);
+        }
+
+        /**
+         * @return all the asset paths of this collected in this class.
+         */
+        public @NonNull String[] getAllAssetPaths() {
+            return mAssetPaths;
+        }
+    }
+
+    public @NonNull ArrayMap<String, SharedLibraryAssets> getSharedLibAssetsMap() {
+        return new ArrayMap<>(mSharedLibAssetsMap);
+    }
 }
diff --git a/core/java/android/app/admin/AccountTypePolicyKey.java b/core/java/android/app/admin/AccountTypePolicyKey.java
index d81eb20..51f3137 100644
--- a/core/java/android/app/admin/AccountTypePolicyKey.java
+++ b/core/java/android/app/admin/AccountTypePolicyKey.java
@@ -19,12 +19,12 @@
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_ACCOUNT_TYPE;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_BUNDLE_KEY;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_KEY;
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
 import android.os.Bundle;
 import android.os.Parcel;
 
@@ -54,7 +54,7 @@
     @TestApi
     public AccountTypePolicyKey(@NonNull String key, @NonNull String accountType) {
         super(key);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxStringLength(accountType, "accountType");
         }
         mAccountType = Objects.requireNonNull((accountType));
diff --git a/core/java/android/app/admin/BundlePolicyValue.java b/core/java/android/app/admin/BundlePolicyValue.java
index cc5e75f..cb5e986 100644
--- a/core/java/android/app/admin/BundlePolicyValue.java
+++ b/core/java/android/app/admin/BundlePolicyValue.java
@@ -16,10 +16,9 @@
 
 package android.app.admin;
 
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.admin.flags.Flags;
 import android.os.Bundle;
 import android.os.Parcel;
 
@@ -32,8 +31,8 @@
 
     public BundlePolicyValue(Bundle value) {
         super(value);
-        if (devicePolicySizeTrackingEnabled()) {
-            PolicySizeVerifier.enforceMaxParcelableFieldsLength(value);
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
+            PolicySizeVerifier.enforceMaxBundleFieldsLength(value);
         }
     }
 
diff --git a/core/java/android/app/admin/ComponentNamePolicyValue.java b/core/java/android/app/admin/ComponentNamePolicyValue.java
index 4d36195..a957dbf 100644
--- a/core/java/android/app/admin/ComponentNamePolicyValue.java
+++ b/core/java/android/app/admin/ComponentNamePolicyValue.java
@@ -16,10 +16,9 @@
 
 package android.app.admin;
 
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.admin.flags.Flags;
 import android.content.ComponentName;
 import android.os.Parcel;
 
@@ -32,7 +31,7 @@
 
     public ComponentNamePolicyValue(@NonNull ComponentName value) {
         super(value);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxComponentNameLength(value);
         }
     }
diff --git a/core/java/android/app/admin/IntentFilterPolicyKey.java b/core/java/android/app/admin/IntentFilterPolicyKey.java
index de7ff9f..7526a7b 100644
--- a/core/java/android/app/admin/IntentFilterPolicyKey.java
+++ b/core/java/android/app/admin/IntentFilterPolicyKey.java
@@ -19,7 +19,6 @@
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_INTENT_FILTER;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_BUNDLE_KEY;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_KEY;
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -60,9 +59,6 @@
     @TestApi
     public IntentFilterPolicyKey(@NonNull String identifier, @NonNull IntentFilter filter) {
         super(identifier);
-        if (devicePolicySizeTrackingEnabled()) {
-            PolicySizeVerifier.enforceMaxParcelableFieldsLength(filter);
-        }
         mFilter = Objects.requireNonNull(filter);
     }
 
diff --git a/core/java/android/app/admin/LockTaskPolicy.java b/core/java/android/app/admin/LockTaskPolicy.java
index 9d6ce24..a36ea05 100644
--- a/core/java/android/app/admin/LockTaskPolicy.java
+++ b/core/java/android/app/admin/LockTaskPolicy.java
@@ -16,11 +16,10 @@
 
 package android.app.admin;
 
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.admin.flags.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -136,7 +135,7 @@
     }
 
     private void setPackagesInternal(Set<String> packages) {
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             for (String p : packages) {
                 PolicySizeVerifier.enforceMaxPackageNameLength(p);
             }
diff --git a/core/java/android/app/admin/PackagePermissionPolicyKey.java b/core/java/android/app/admin/PackagePermissionPolicyKey.java
index 2241fdd..389585f 100644
--- a/core/java/android/app/admin/PackagePermissionPolicyKey.java
+++ b/core/java/android/app/admin/PackagePermissionPolicyKey.java
@@ -20,12 +20,12 @@
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_PERMISSION_NAME;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_BUNDLE_KEY;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_KEY;
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -59,7 +59,7 @@
     public PackagePermissionPolicyKey(@NonNull String identifier, @NonNull String packageName,
             @NonNull String permissionName) {
         super(identifier);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxPackageNameLength(packageName);
             PolicySizeVerifier.enforceMaxStringLength(permissionName, "permissionName");
         }
diff --git a/core/java/android/app/admin/PackagePolicyKey.java b/core/java/android/app/admin/PackagePolicyKey.java
index 2ea17a1..68dc797 100644
--- a/core/java/android/app/admin/PackagePolicyKey.java
+++ b/core/java/android/app/admin/PackagePolicyKey.java
@@ -19,12 +19,12 @@
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_PACKAGE_NAME;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_BUNDLE_KEY;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_KEY;
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -55,7 +55,7 @@
     @TestApi
     public PackagePolicyKey(@NonNull String key, @NonNull String packageName) {
         super(key);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxPackageNameLength(packageName);
         }
         mPackageName = Objects.requireNonNull((packageName));
diff --git a/core/java/android/app/admin/PolicySizeVerifier.java b/core/java/android/app/admin/PolicySizeVerifier.java
index 792ebc6..7f8e50e 100644
--- a/core/java/android/app/admin/PolicySizeVerifier.java
+++ b/core/java/android/app/admin/PolicySizeVerifier.java
@@ -17,12 +17,12 @@
 package android.app.admin;
 
 import android.content.ComponentName;
+import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 
 import com.android.internal.util.Preconditions;
 
-import java.lang.reflect.Field;
 import java.util.ArrayDeque;
 import java.util.Queue;
 
@@ -71,44 +71,51 @@
             for (String key : current.keySet()) {
                 enforceMaxStringLength(key, "key in " + argName);
                 Object value = current.get(key);
-                if (value instanceof String) {
-                    enforceMaxStringLength((String) value, "string value in " + argName);
-                } else if (value instanceof String[]) {
-                    for (String str : (String[]) value) {
+                if (value instanceof String str) {
+                    enforceMaxStringLength(str, "string value in " + argName);
+                } else if (value instanceof String[] strArray) {
+                    for (String str : strArray) {
                         enforceMaxStringLength(str, "string value in " + argName);
                     }
-                } else if (value instanceof PersistableBundle) {
-                    queue.add((PersistableBundle) value);
+                } else if (value instanceof PersistableBundle persistableBundle) {
+                    queue.add(persistableBundle);
                 }
             }
         }
     }
 
     /**
-     * Throw if Parcelable contains any string that's too long to be serialized.
+     * Throw if bundle contains any string that's too long to be serialized. This follows the
+     * serialization logic in BundlePolicySerializer#writeBundle.
      */
-    public static void enforceMaxParcelableFieldsLength(Parcelable parcelable) {
-        // TODO(b/326662716) rework to protect against infinite recursion.
-        if (true) {
-            return;
-        }
-        Class<?> clazz = parcelable.getClass();
-
-        Field[] fields = clazz.getDeclaredFields();
-        for (Field field : fields) {
-            field.setAccessible(true);
-            try {
-                Object value = field.get(parcelable);
-                if (value instanceof String) {
-                    String stringValue = (String) value;
-                    enforceMaxStringLength(stringValue, field.getName());
+    public static void enforceMaxBundleFieldsLength(Bundle bundle) {
+        Queue<Bundle> queue = new ArrayDeque<>();
+        queue.add(bundle);
+        while (!queue.isEmpty()) {
+            Bundle current = queue.remove();
+            for (String key : current.keySet()) {
+                enforceMaxStringLength(key, "key in Bundle");
+                Object value = current.get(key);
+                if (value instanceof String str) {
+                    enforceMaxStringLength(str, "string value in Bundle with "
+                            + "key" + key);
+                } else if (value instanceof String[] strArray) {
+                    for (String str : strArray) {
+                        enforceMaxStringLength(str, "string value in Bundle with"
+                                + " key" + key);
+                    }
+                } else if (value instanceof Bundle b) {
+                    queue.add(b);
                 }
-
-                if (value instanceof Parcelable) {
-                    enforceMaxParcelableFieldsLength((Parcelable) value);
+                else if (value instanceof Parcelable[] parcelableArray) {
+                    for (Parcelable parcelable : parcelableArray) {
+                        if (!(parcelable instanceof Bundle)) {
+                            throw new IllegalArgumentException("bundle-array can only hold "
+                                    + "Bundles");
+                        }
+                        queue.add((Bundle) parcelable);
+                    }
                 }
-            } catch (IllegalAccessException e) {
-                e.printStackTrace();
             }
         }
     }
diff --git a/core/java/android/app/admin/StringPolicyValue.java b/core/java/android/app/admin/StringPolicyValue.java
index f4d4adc..8995c0f 100644
--- a/core/java/android/app/admin/StringPolicyValue.java
+++ b/core/java/android/app/admin/StringPolicyValue.java
@@ -16,10 +16,9 @@
 
 package android.app.admin;
 
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.admin.flags.Flags;
 import android.os.Parcel;
 
 import java.util.Objects;
@@ -31,7 +30,7 @@
 
     public StringPolicyValue(@NonNull String value) {
         super(value);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxStringLength(value, "policyValue");
         }
     }
diff --git a/core/java/android/app/admin/StringSetPolicyValue.java b/core/java/android/app/admin/StringSetPolicyValue.java
index 82fe761..f37dfee 100644
--- a/core/java/android/app/admin/StringSetPolicyValue.java
+++ b/core/java/android/app/admin/StringSetPolicyValue.java
@@ -16,10 +16,9 @@
 
 package android.app.admin;
 
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.admin.flags.Flags;
 import android.os.Parcel;
 
 import java.util.HashSet;
@@ -33,7 +32,7 @@
 
     public StringSetPolicyValue(@NonNull Set<String> value) {
         super(value);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             for (String str : value) {
                 PolicySizeVerifier.enforceMaxStringLength(str, "policyValue");
             }
diff --git a/core/java/android/app/admin/UserRestrictionPolicyKey.java b/core/java/android/app/admin/UserRestrictionPolicyKey.java
index d69a5f0..ee90ccd 100644
--- a/core/java/android/app/admin/UserRestrictionPolicyKey.java
+++ b/core/java/android/app/admin/UserRestrictionPolicyKey.java
@@ -17,11 +17,11 @@
 package android.app.admin;
 
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_KEY;
-import static android.app.admin.flags.Flags.devicePolicySizeTrackingEnabled;
 
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
 import android.os.Bundle;
 import android.os.Parcel;
 
@@ -45,7 +45,7 @@
     @TestApi
     public UserRestrictionPolicyKey(@NonNull String identifier, @NonNull String restriction) {
         super(identifier);
-        if (devicePolicySizeTrackingEnabled()) {
+        if (Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxStringLength(restriction, "restriction");
         }
         mRestriction = Objects.requireNonNull(restriction);
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index e1a6913..1927019 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -10,7 +10,14 @@
 flag {
   name: "device_policy_size_tracking_enabled"
   namespace: "enterprise"
-  description: "Add feature to track the total policy size and have a max threshold."
+  description: "Add feature to track the total policy size and have a max threshold - public API changes"
+  bug: "281543351"
+}
+
+flag {
+  name: "device_policy_size_tracking_internal_enabled"
+  namespace: "enterprise"
+  description: "Add feature to track the total policy size and have a max threshold - internal changes"
   bug: "281543351"
 }
 
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index fb1b17b..89199ca 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -1,5 +1,7 @@
 package android.app.assist;
 
+import static android.credentials.Constants.FAILURE_CREDMAN_SELECTOR;
+import static android.credentials.Constants.SUCCESS_CREDMAN_SELECTOR;
 import static android.service.autofill.Flags.FLAG_AUTOFILL_CREDMAN_DEV_INTEGRATION;
 
 import android.annotation.FlaggedApi;
@@ -20,14 +22,17 @@
 import android.os.BadParcelableException;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.LocaleList;
+import android.os.Looper;
 import android.os.OutcomeReceiver;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.PooledStringReader;
 import android.os.PooledStringWriter;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.service.autofill.FillRequest;
 import android.service.credentials.CredentialProviderService;
@@ -37,6 +42,7 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Slog;
 import android.view.View;
 import android.view.View.AutofillImportance;
 import android.view.ViewRootImpl;
@@ -652,6 +658,9 @@
         @Nullable OutcomeReceiver<GetCredentialResponse, GetCredentialException>
                 mGetCredentialCallback;
 
+        @Nullable ResultReceiver mGetCredentialResultReceiver;
+
+
         AutofillValue mAutofillValue;
         CharSequence[] mAutofillOptions;
         boolean mSanitized;
@@ -916,6 +925,7 @@
                 mExtras = in.readBundle();
             }
             mGetCredentialRequest = in.readTypedObject(GetCredentialRequest.CREATOR);
+            mGetCredentialResultReceiver = in.readTypedObject(ResultReceiver.CREATOR);
         }
 
         /**
@@ -1153,6 +1163,7 @@
                 out.writeBundle(mExtras);
             }
             out.writeTypedObject(mGetCredentialRequest, flags);
+            out.writeTypedObject(mGetCredentialResultReceiver, flags);
             return flags;
         }
 
@@ -1295,9 +1306,8 @@
          */
         @FlaggedApi(FLAG_AUTOFILL_CREDMAN_DEV_INTEGRATION)
         @Nullable
-        public OutcomeReceiver<GetCredentialResponse,
-                GetCredentialException> getCredentialManagerCallback() {
-            return mGetCredentialCallback;
+        public ResultReceiver getCredentialManagerCallback() {
+            return mGetCredentialResultReceiver;
         }
 
         /**
@@ -1894,6 +1904,7 @@
         final AssistStructure mAssist;
         final ViewNode mNode;
         final boolean mAsync;
+        private Handler mHandler;
 
         /**
          * Used to instantiate a builder for a stand-alone {@link ViewNode} which is not associated
@@ -2271,6 +2282,56 @@
                 option.getCandidateQueryData()
                         .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids);
             }
+            setUpResultReceiver(callback);
+        }
+
+        private void setUpResultReceiver(
+                OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
+
+            if (mHandler == null) {
+                mHandler = new Handler(Looper.getMainLooper(), null, true);
+            }
+            final ResultReceiver resultReceiver = new ResultReceiver(mHandler) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle resultData) {
+                    if (resultCode == SUCCESS_CREDMAN_SELECTOR) {
+                        Slog.d(TAG, "onReceiveResult from Credential Manager");
+                        GetCredentialResponse getCredentialResponse =
+                                resultData.getParcelable(
+                                        CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+                                        GetCredentialResponse.class);
+
+                        callback.onResult(getCredentialResponse);
+                    } else if (resultCode == FAILURE_CREDMAN_SELECTOR) {
+                        String[] exception =  resultData.getStringArray(
+                                CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION);
+                        if (exception != null && exception.length >= 2) {
+                            Slog.w(TAG, "Credman bottom sheet from pinned "
+                                    + "entry failed with: + " + exception[0] + " , "
+                                    + exception[1]);
+                            callback.onError(new GetCredentialException(
+                                    exception[0], exception[1]));
+                        }
+                    } else {
+                        Slog.d(TAG, "Unknown resultCode from credential "
+                                + "manager bottom sheet: " + resultCode);
+                    }
+                }
+            };
+            ResultReceiver ipcFriendlyResultReceiver =
+                    toIpcFriendlyResultReceiver(resultReceiver);
+            mNode.mGetCredentialResultReceiver = ipcFriendlyResultReceiver;
+        }
+
+        private ResultReceiver toIpcFriendlyResultReceiver(ResultReceiver resultReceiver) {
+            final Parcel parcel = Parcel.obtain();
+            resultReceiver.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+
+            final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+            parcel.recycle();
+
+            return ipcFriendly;
         }
 
         @Override
diff --git a/core/java/android/app/ondeviceintelligence/Feature.java b/core/java/android/app/ondeviceintelligence/Feature.java
index 5107354..4a38c92 100644
--- a/core/java/android/app/ondeviceintelligence/Feature.java
+++ b/core/java/android/app/ondeviceintelligence/Feature.java
@@ -199,24 +199,13 @@
 
         private long mBuilderFieldsSet = 0L;
 
-        public Builder(
-                int id,
-                int type,
-                int variant,
-                @NonNull PersistableBundle featureParams) {
+        /**
+         * Provides a builder instance to create a feature for given id.
+         * @param id the unique identifier for the feature.
+         */
+        public Builder(int id) {
             mId = id;
-            mType = type;
-            mVariant = variant;
-            mFeatureParams = featureParams;
-            com.android.internal.util.AnnotationValidations.validate(
-                    NonNull.class, null, mFeatureParams);
-        }
-
-        public @NonNull Builder setId(int value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x1;
-            mId = value;
-            return this;
+            mFeatureParams = new PersistableBundle();
         }
 
         public @NonNull Builder setName(@NonNull String value) {
diff --git a/core/java/android/app/ondeviceintelligence/FeatureDetails.java b/core/java/android/app/ondeviceintelligence/FeatureDetails.java
index 92f3513..f3cbd26 100644
--- a/core/java/android/app/ondeviceintelligence/FeatureDetails.java
+++ b/core/java/android/app/ondeviceintelligence/FeatureDetails.java
@@ -41,7 +41,7 @@
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
 public final class FeatureDetails implements Parcelable {
     @Status
-    private final int mStatus;
+    private final int mFeatureStatus;
     @NonNull
     private final PersistableBundle mFeatureDetailParams;
 
@@ -73,21 +73,21 @@
     }
 
     public FeatureDetails(
-            @Status int status,
+            @Status int featureStatus,
             @NonNull PersistableBundle featureDetailParams) {
-        this.mStatus = status;
+        this.mFeatureStatus = featureStatus;
         com.android.internal.util.AnnotationValidations.validate(
-                Status.class, null, mStatus);
+                Status.class, null, mFeatureStatus);
         this.mFeatureDetailParams = featureDetailParams;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mFeatureDetailParams);
     }
 
     public FeatureDetails(
-            @Status int status) {
-        this.mStatus = status;
+            @Status int featureStatus) {
+        this.mFeatureStatus = featureStatus;
         com.android.internal.util.AnnotationValidations.validate(
-                Status.class, null, mStatus);
+                Status.class, null, mFeatureStatus);
         this.mFeatureDetailParams = new PersistableBundle();
     }
 
@@ -95,8 +95,8 @@
     /**
      * Returns an integer value associated with the feature status.
      */
-    public @Status int getStatus() {
-        return mStatus;
+    public @Status int getFeatureStatus() {
+        return mFeatureStatus;
     }
 
 
@@ -111,7 +111,7 @@
     public String toString() {
         return MessageFormat.format("FeatureDetails '{' status = {0}, "
                         + "persistableBundle = {1} '}'",
-                mStatus,
+                mFeatureStatus,
                 mFeatureDetailParams);
     }
 
@@ -121,21 +121,21 @@
         if (o == null || getClass() != o.getClass()) return false;
         @SuppressWarnings("unchecked")
         FeatureDetails that = (FeatureDetails) o;
-        return mStatus == that.mStatus
+        return mFeatureStatus == that.mFeatureStatus
                 && java.util.Objects.equals(mFeatureDetailParams, that.mFeatureDetailParams);
     }
 
     @Override
     public int hashCode() {
         int _hash = 1;
-        _hash = 31 * _hash + mStatus;
+        _hash = 31 * _hash + mFeatureStatus;
         _hash = 31 * _hash + java.util.Objects.hashCode(mFeatureDetailParams);
         return _hash;
     }
 
     @Override
     public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
-        dest.writeInt(mStatus);
+        dest.writeInt(mFeatureStatus);
         dest.writeTypedObject(mFeatureDetailParams, flags);
     }
 
@@ -151,9 +151,9 @@
         PersistableBundle persistableBundle = (PersistableBundle) in.readTypedObject(
                 PersistableBundle.CREATOR);
 
-        this.mStatus = status;
+        this.mFeatureStatus = status;
         com.android.internal.util.AnnotationValidations.validate(
-                Status.class, null, mStatus);
+                Status.class, null, mFeatureStatus);
         this.mFeatureDetailParams = persistableBundle;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mFeatureDetailParams);
diff --git a/core/java/android/app/ondeviceintelligence/FilePart.java b/core/java/android/app/ondeviceintelligence/FilePart.java
deleted file mode 100644
index e9fb5f2..0000000
--- a/core/java/android/app/ondeviceintelligence/FilePart.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.ondeviceintelligence;
-
-import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
-
-import android.annotation.FlaggedApi;
-import android.annotation.SystemApi;
-import android.os.Parcel;
-import android.os.ParcelFileDescriptor;
-import android.os.Parcelable;
-import android.os.PersistableBundle;
-
-import android.annotation.NonNull;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Objects;
-
-/**
- * Represents file data with an associated file descriptor sent to and received from remote
- * processing. The interface ensures that the underlying file-descriptor is always opened in
- * read-only mode.
- *
- * @hide
- */
-@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
-@SystemApi
-public final class FilePart implements Parcelable {
-    private final String mPartKey;
-    private final PersistableBundle mPartParams;
-    private final ParcelFileDescriptor mParcelFileDescriptor;
-
-    private FilePart(@NonNull String partKey, @NonNull PersistableBundle partParams,
-            @NonNull ParcelFileDescriptor parcelFileDescriptor) {
-        Objects.requireNonNull(partKey);
-        Objects.requireNonNull(partParams);
-        this.mPartKey = partKey;
-        this.mPartParams = partParams;
-        this.mParcelFileDescriptor = Objects.requireNonNull(parcelFileDescriptor);
-    }
-
-    /**
-     * Create a file part using a filePath and any additional params.
-     */
-    public FilePart(@NonNull String partKey, @NonNull PersistableBundle partParams,
-            @NonNull String filePath)
-            throws FileNotFoundException {
-        this(partKey, partParams, Objects.requireNonNull(ParcelFileDescriptor.open(
-                new File(Objects.requireNonNull(filePath)), ParcelFileDescriptor.MODE_READ_ONLY)));
-    }
-
-    /**
-     * Create a file part using a file input stream and any additional params.
-     * It is the caller's responsibility to close the stream. It is safe to do so as soon as this
-     * call returns.
-     */
-    public FilePart(@NonNull String partKey, @NonNull PersistableBundle partParams,
-            @NonNull FileInputStream fileInputStream)
-            throws IOException {
-        this(partKey, partParams, ParcelFileDescriptor.dup(fileInputStream.getFD()));
-    }
-
-    /**
-     * Returns a FileInputStream for the associated File.
-     * Caller must close the associated stream when done reading from it.
-     *
-     * @return the FileInputStream associated with the FilePart.
-     */
-    @NonNull
-    public FileInputStream getFileInputStream() {
-        return new FileInputStream(mParcelFileDescriptor.getFileDescriptor());
-    }
-
-    /**
-     * Returns the unique key associated with the part. Each Part key added to a content object
-     * should be ensured to be unique.
-     */
-    @NonNull
-    public String getFilePartKey() {
-        return mPartKey;
-    }
-
-    /**
-     * Returns the params associated with Part.
-     */
-    @NonNull
-    public PersistableBundle getFilePartParams() {
-        return mPartParams;
-    }
-
-
-    @Override
-    public int describeContents() {
-        return CONTENTS_FILE_DESCRIPTOR;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString8(getFilePartKey());
-        dest.writePersistableBundle(getFilePartParams());
-        mParcelFileDescriptor.writeToParcel(dest, flags
-                | Parcelable.PARCELABLE_WRITE_RETURN_VALUE); // This flag ensures that the sender's
-        // copy of the Pfd is closed as soon as the Binder call succeeds.
-    }
-
-    @NonNull
-    public static final Creator<FilePart> CREATOR = new Creator<>() {
-        @Override
-        public FilePart createFromParcel(Parcel in) {
-            return new FilePart(in.readString(), in.readTypedObject(PersistableBundle.CREATOR),
-                    in.readParcelable(
-                            getClass().getClassLoader(), ParcelFileDescriptor.class));
-        }
-
-        @Override
-        public FilePart[] newArray(int size) {
-            return new FilePart[size];
-        }
-    };
-}
diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
index b925f48..360a809 100644
--- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
+++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -31,7 +31,7 @@
  import android.app.ondeviceintelligence.IResponseCallback;
  import android.app.ondeviceintelligence.IStreamingResponseCallback;
  import android.app.ondeviceintelligence.IProcessingSignal;
- import android.app.ondeviceintelligence.ITokenCountCallback;
+ import android.app.ondeviceintelligence.ITokenInfoCallback;
 
 
  /**
@@ -56,8 +56,8 @@
       void requestFeatureDownload(in Feature feature, ICancellationSignal signal, in IDownloadCallback callback) = 5;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void requestTokenCount(in Feature feature, in Content request, in  ICancellationSignal signal,
-                                                        in ITokenCountCallback tokenCountcallback) = 6;
+      void requestTokenInfo(in Feature feature, in Content request, in  ICancellationSignal signal,
+                                                        in ITokenInfoCallback tokenInfocallback) = 6;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
       void processRequest(in Feature feature, in Content request, int requestType, in  ICancellationSignal cancellationSignal, in IProcessingSignal signal,
diff --git a/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl
index 9848e1d..0adf305 100644
--- a/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl
+++ b/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl
@@ -3,6 +3,7 @@
 import android.app.ondeviceintelligence.Content;
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.os.PersistableBundle;
+import android.os.RemoteCallback;
 
 /**
   * Interface for a IResponseCallback for receiving response from on-device intelligence service.
@@ -12,4 +13,5 @@
 interface IResponseCallback {
     void onSuccess(in Content result) = 1;
     void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2;
+    void onDataAugmentRequest(in Content content, in RemoteCallback contentCallback) = 3;
 }
diff --git a/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
index a680574..132e53e 100644
--- a/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
+++ b/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
@@ -4,6 +4,7 @@
 import android.app.ondeviceintelligence.IResponseCallback;
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.os.PersistableBundle;
+import android.os.RemoteCallback;
 
 
 /**
@@ -15,4 +16,5 @@
     void onNewContent(in Content result) = 1;
     void onSuccess(in Content result) = 2;
     void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 3;
+    void onDataAugmentRequest(in Content content, in RemoteCallback contentCallback) = 4;
 }
diff --git a/core/java/android/app/ondeviceintelligence/ITokenCountCallback.aidl b/core/java/android/app/ondeviceintelligence/ITokenCountCallback.aidl
deleted file mode 100644
index b724e03..0000000
--- a/core/java/android/app/ondeviceintelligence/ITokenCountCallback.aidl
+++ /dev/null
@@ -1,13 +0,0 @@
-package android.app.ondeviceintelligence;
-
-import android.os.PersistableBundle;
-
-/**
-  * Interface for receiving the token count of a request for a given features.
-  *
-  * @hide
-  */
-interface ITokenCountCallback {
-    void onSuccess(long tokenCount) = 1;
-    void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2;
-}
diff --git a/core/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl b/core/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
new file mode 100644
index 0000000..9219a89
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
@@ -0,0 +1,14 @@
+package android.app.ondeviceintelligence;
+
+import android.os.PersistableBundle;
+import android.app.ondeviceintelligence.TokenInfo;
+
+/**
+  * Interface for receiving the token info of a request for a given feature.
+  *
+  * @hide
+  */
+interface ITokenInfoCallback {
+    void onSuccess(in TokenInfo tokenInfo) = 1;
+    void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2;
+}
diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
index 4d8e0d5..d195c4d 100644
--- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
+++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -26,8 +26,10 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
@@ -37,6 +39,8 @@
 
 import androidx.annotation.IntDef;
 
+import com.android.internal.R;
+
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -60,7 +64,16 @@
 @SystemService(Context.ON_DEVICE_INTELLIGENCE_SERVICE)
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
 public class OnDeviceIntelligenceManager {
+    /**
+     * @hide
+     */
     public static final String API_VERSION_BUNDLE_KEY = "ApiVersionBundleKey";
+
+    /**
+     * @hide
+     */
+    public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY =
+            "AugmentRequestContentBundleKey";
     private final Context mContext;
     private final IOnDeviceIntelligenceManager mService;
 
@@ -82,8 +95,6 @@
     public void getVersion(
             @NonNull @CallbackExecutor Executor callbackExecutor,
             @NonNull LongConsumer versionConsumer) {
-        // TODO explore modifying this method into getServicePackageDetails and return both
-        //  version and package name of the remote service implementing this.
         try {
             RemoteCallback callback = new RemoteCallback(result -> {
                 if (result == null) {
@@ -100,6 +111,23 @@
         }
     }
 
+
+    /**
+     * Get package name configured for providing the remote implementation for this system service.
+     */
+    @Nullable
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public String getRemoteServicePackageName() {
+        String serviceConfigValue = mContext.getResources().getString(
+                R.string.config_defaultOnDeviceSandboxedInferenceService);
+        ComponentName componentName = ComponentName.unflattenFromString(serviceConfigValue);
+        if (componentName != null) {
+            return componentName.getPackageName();
+        }
+
+        return null;
+    }
+
     /**
      * Asynchronously get feature for a given id.
      *
@@ -273,29 +301,29 @@
     }
 
     /**
-     * The methods computes the token-count for a given request payload using the provided Feature
-     * details.
+     * The methods computes the token related information for a given request payload using the
+     * provided {@link Feature}.
      *
      * @param feature            feature associated with the request.
      * @param request            request that contains the content data and associated params.
-     * @param outcomeReceiver    callback to populate the token count or exception in case of
+     * @param outcomeReceiver    callback to populate the token info or exception in case of
      *                           failure.
      * @param cancellationSignal signal to invoke cancellation on the operation in the remote
      *                           implementation.
      * @param callbackExecutor   executor to run the callback on.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
-    public void requestTokenCount(@NonNull Feature feature, @NonNull Content request,
+    public void requestTokenInfo(@NonNull Feature feature, @NonNull Content request,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull @CallbackExecutor Executor callbackExecutor,
-            @NonNull OutcomeReceiver<Long,
+            @NonNull OutcomeReceiver<TokenInfo,
                     OnDeviceIntelligenceManagerException> outcomeReceiver) {
         try {
-            ITokenCountCallback callback = new ITokenCountCallback.Stub() {
+            ITokenInfoCallback callback = new ITokenInfoCallback.Stub() {
                 @Override
-                public void onSuccess(long tokenCount) {
+                public void onSuccess(TokenInfo tokenInfo) {
                     Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
-                            () -> outcomeReceiver.onResult(tokenCount)));
+                            () -> outcomeReceiver.onResult(tokenInfo)));
                 }
 
                 @Override
@@ -314,7 +342,7 @@
                 cancellationSignal.setRemote(transport);
             }
 
-            mService.requestTokenCount(feature, request, transport, callback);
+            mService.requestTokenInfo(feature, request, transport, callback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -328,33 +356,31 @@
      * was a
      * failure.
      *
-     * @param feature                 feature associated with the request.
-     * @param request                 request that contains the Content data and
-     *                                associated params.
-     * @param requestType             type of request being sent for processing the content.
-     * @param responseOutcomeReceiver callback to populate the response content and
-     *                                associated
-     *                                params.
-     * @param processingSignal        signal to invoke custom actions in the
-     *                                remote implementation.
-     * @param cancellationSignal      signal to invoke cancellation or
-     * @param callbackExecutor        executor to run the callback on.
+     * @param feature            feature associated with the request.
+     * @param request            request that contains the Content data and
+     *                           associated params.
+     * @param requestType        type of request being sent for processing the content.
+     * @param cancellationSignal signal to invoke cancellation.
+     * @param processingSignal   signal to send custom signals in the
+     *                           remote implementation.
+     * @param callbackExecutor   executor to run the callback on.
+     * @param responseCallback   callback to populate the response content and
+     *                           associated params.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
 
-    public void processRequest(@NonNull Feature feature, @NonNull Content request,
+    public void processRequest(@NonNull Feature feature, @Nullable Content request,
             @RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
             @NonNull @CallbackExecutor Executor callbackExecutor,
-            @NonNull OutcomeReceiver<Content,
-                    OnDeviceIntelligenceManagerProcessingException> responseOutcomeReceiver) {
+            @NonNull ProcessingOutcomeReceiver responseCallback) {
         try {
             IResponseCallback callback = new IResponseCallback.Stub() {
                 @Override
                 public void onSuccess(Content result) {
                     Binder.withCleanCallingIdentity(() -> {
-                        callbackExecutor.execute(() -> responseOutcomeReceiver.onResult(result));
+                        callbackExecutor.execute(() -> responseCallback.onResult(result));
                     });
                 }
 
@@ -362,12 +388,24 @@
                 public void onFailure(int errorCode, String errorMessage,
                         PersistableBundle errorParams) {
                     Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
-                            () -> responseOutcomeReceiver.onError(
+                            () -> responseCallback.onError(
                                     new OnDeviceIntelligenceManagerProcessingException(
                                             errorCode, errorMessage, errorParams))));
                 }
+
+                @Override
+                public void onDataAugmentRequest(@NonNull Content content,
+                        @NonNull RemoteCallback contentCallback) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> responseCallback.onDataAugmentRequest(content, result -> {
+                                Bundle bundle = new Bundle();
+                                bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, result);
+                                callbackExecutor.execute(() -> contentCallback.sendResult(bundle));
+                            })));
+                }
             };
 
+
             IProcessingSignal transport = null;
             if (processingSignal != null) {
                 transport = ProcessingSignal.createTransport();
@@ -389,46 +427,48 @@
     }
 
     /**
-     * Variation of {@link #processRequest} that asynchronously processes a request in a streaming
+     * Variation of {@link #processRequest} that asynchronously processes a request in a
+     * streaming
      * fashion, where new content is pushed to caller in chunks via the
-     * {@link StreamingResponseReceiver#onNewContent}. After the streaming is complete,
-     * the service should call {@link StreamingResponseReceiver#onResult} and can optionally
-     * populate the complete {@link Response}'s Content as part of the callback when the final
-     * {@link Response} contains an enhanced aggregation of the Contents already streamed.
+     * {@link StreamedProcessingOutcomeReceiver#onNewContent}. After the streaming is complete,
+     * the service should call {@link StreamedProcessingOutcomeReceiver#onResult} and can optionally
+     * populate the complete the full response {@link Content} as part of the callback in cases
+     * when the final response contains an enhanced aggregation of the Contents already
+     * streamed.
      *
      * @param feature                   feature associated with the request.
      * @param request                   request that contains the Content data and associated
      *                                  params.
      * @param requestType               type of request being sent for processing the content.
-     * @param processingSignal          signal to invoke  other custom actions in the
+     * @param cancellationSignal        signal to invoke cancellation.
+     * @param processingSignal          signal to send custom signals in the
      *                                  remote implementation.
-     * @param cancellationSignal        signal to invoke cancellation
-     * @param streamingResponseReceiver streaming callback to populate the response content and
+     * @param streamingResponseCallback streaming callback to populate the response content and
      *                                  associated params.
      * @param callbackExecutor          executor to run the callback on.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
-    public void processRequestStreaming(@NonNull Feature feature, @NonNull Content request,
+    public void processRequestStreaming(@NonNull Feature feature, @Nullable Content request,
             @RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
             @NonNull @CallbackExecutor Executor callbackExecutor,
-            @NonNull StreamingResponseReceiver<Content, Content,
-                    OnDeviceIntelligenceManagerProcessingException> streamingResponseReceiver) {
+            @NonNull StreamedProcessingOutcomeReceiver streamingResponseCallback) {
         try {
             IStreamingResponseCallback callback = new IStreamingResponseCallback.Stub() {
                 @Override
                 public void onNewContent(Content result) {
                     Binder.withCleanCallingIdentity(() -> {
                         callbackExecutor.execute(
-                                () -> streamingResponseReceiver.onNewContent(result));
+                                () -> streamingResponseCallback.onNewContent(result));
                     });
                 }
 
                 @Override
                 public void onSuccess(Content result) {
                     Binder.withCleanCallingIdentity(() -> {
-                        callbackExecutor.execute(() -> streamingResponseReceiver.onResult(result));
+                        callbackExecutor.execute(
+                                () -> streamingResponseCallback.onResult(result));
                     });
                 }
 
@@ -437,11 +477,26 @@
                         PersistableBundle errorParams) {
                     Binder.withCleanCallingIdentity(() -> {
                         callbackExecutor.execute(
-                                () -> streamingResponseReceiver.onError(
+                                () -> streamingResponseCallback.onError(
                                         new OnDeviceIntelligenceManagerProcessingException(
                                                 errorCode, errorMessage, errorParams)));
                     });
                 }
+
+
+                @Override
+                public void onDataAugmentRequest(@NonNull Content content,
+                        @NonNull RemoteCallback contentCallback) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> streamingResponseCallback.onDataAugmentRequest(content,
+                                    contentResponse -> {
+                                        Bundle bundle = new Bundle();
+                                        bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY,
+                                                contentResponse);
+                                        callbackExecutor.execute(
+                                                () -> contentCallback.sendResult(bundle));
+                                    })));
+                }
             };
 
             IProcessingSignal transport = null;
@@ -468,7 +523,8 @@
     public static final int REQUEST_TYPE_INFERENCE = 0;
 
     /**
-     * Prepares the remote implementation environment for e.g.loading inference runtime etc.which
+     * Prepares the remote implementation environment for e.g.loading inference runtime etc
+     * .which
      * are time consuming beforehand to remove overhead and allow quick processing of requests
      * thereof.
      */
@@ -485,7 +541,8 @@
             REQUEST_TYPE_PREPARE,
             REQUEST_TYPE_EMBEDDINGS
     }, open = true)
-    @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
+    @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER,
+            ElementType.FIELD})
     @Retention(RetentionPolicy.SOURCE)
     public @interface RequestType {
     }
@@ -501,17 +558,32 @@
          */
         public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 1000;
 
+        /**
+         * Error code to be used for on device intelligence manager failures.
+         *
+         * @hide
+         */
+        @IntDef(
+                value = {
+                        ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE
+                }, open = true)
+        @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+        @interface OnDeviceIntelligenceManagerErrorCode {
+        }
+
         private final int mErrorCode;
         private final PersistableBundle errorParams;
 
-        public OnDeviceIntelligenceManagerException(int errorCode, @NonNull String errorMessage,
+        public OnDeviceIntelligenceManagerException(
+                @OnDeviceIntelligenceManagerErrorCode int errorCode, @NonNull String errorMessage,
                 @NonNull PersistableBundle errorParams) {
             super(errorMessage);
             this.mErrorCode = errorCode;
             this.errorParams = errorParams;
         }
 
-        public OnDeviceIntelligenceManagerException(int errorCode,
+        public OnDeviceIntelligenceManagerException(
+                @OnDeviceIntelligenceManagerErrorCode int errorCode,
                 @NonNull PersistableBundle errorParams) {
             this.mErrorCode = errorCode;
             this.errorParams = errorParams;
@@ -573,11 +645,15 @@
         /** Inference suspended so that higher-priority inference can run. */
         public static final int PROCESSING_ERROR_SUSPENDED = 13;
 
-        /** Underlying processing encountered an internal error, like a violated precondition. */
+        /**
+         * Underlying processing encountered an internal error, like a violated precondition
+         * .
+         */
         public static final int PROCESSING_ERROR_INTERNAL = 14;
 
         /**
-         * The processing was not able to be passed on to the remote implementation, as the service
+         * The processing was not able to be passed on to the remote implementation, as the
+         * service
          * was unavailable.
          */
         public static final int PROCESSING_ERROR_SERVICE_UNAVAILABLE = 15;
diff --git a/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java b/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java
new file mode 100644
index 0000000..b0b6e19
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.OutcomeReceiver;
+
+import java.util.function.Consumer;
+
+/**
+ * Response Callback to populate the processed response or any error that occurred during the
+ * request processing. This callback also provides a method to request additional data to be
+ * augmented to the request-processing, using  the partial {@link Content} that was already
+ * processed in the remote implementation.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public interface ProcessingOutcomeReceiver extends
+        OutcomeReceiver<Content,
+                OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> {
+    /**
+     * Callback to be invoked in cases where the remote service needs to perform retrieval or
+     * transformation operations based on a partially processed request, in order to augment the
+     * final response, by using the additional context sent via this callback.
+     *
+     * @param content         The content payload that should be used to augment ongoing request.
+     * @param contentConsumer The augmentation data that should be sent to remote
+     *                        service for further processing a request.
+     */
+    default void onDataAugmentRequest(@NonNull Content content,
+            @NonNull Consumer<Content> contentConsumer) {
+        contentConsumer.accept(null);
+    }
+}
diff --git a/core/java/android/app/ondeviceintelligence/StreamingResponseReceiver.java b/core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java
similarity index 71%
rename from core/java/android/app/ondeviceintelligence/StreamingResponseReceiver.java
rename to core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java
index ebcf61c..ac2b032 100644
--- a/core/java/android/app/ondeviceintelligence/StreamingResponseReceiver.java
+++ b/core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java
@@ -21,23 +21,19 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
-import android.os.OutcomeReceiver;
 
 /**
  * Streaming variant of outcome receiver to populate response while processing a given request,
- * possibly in
- * chunks to provide a async processing behaviour to the caller.
+ * possibly in chunks to provide a async processing behaviour to the caller.
  *
  * @hide
  */
 @SystemApi
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
-public interface StreamingResponseReceiver<R, T, E extends Throwable> extends
-        OutcomeReceiver<R, E> {
+public interface StreamedProcessingOutcomeReceiver extends ProcessingOutcomeReceiver {
     /**
-     * Callback to be invoked when a part of the response i.e. some {@link Content} is already
-     * processed and
-     * needs to be passed onto the caller.
+     * Callback that would be invoked when a part of the response i.e. some {@link Content} is
+     * already processed and needs to be passed onto the caller.
      */
-    void onNewContent(@NonNull T content);
+    void onNewContent(@NonNull Content content);
 }
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/app/ondeviceintelligence/TokenInfo.aidl
similarity index 74%
rename from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
rename to core/java/android/app/ondeviceintelligence/TokenInfo.aidl
index 838e41e..2c19c1e 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/app/ondeviceintelligence/TokenInfo.aidl
@@ -1,5 +1,5 @@
-/**
- * Copyright (c) 2024, The Android Open Source Project
+/*
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.app.ondeviceintelligence;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
-}
-
+/**
+  * @hide
+  */
+parcelable TokenInfo;
diff --git a/core/java/android/app/ondeviceintelligence/TokenInfo.java b/core/java/android/app/ondeviceintelligence/TokenInfo.java
new file mode 100644
index 0000000..035cc4b
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/TokenInfo.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+
+/**
+ * This class is used to provide a token count response for the
+ * {@link OnDeviceIntelligenceManager#requestTokenInfo} outcome receiver.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public final class TokenInfo implements Parcelable {
+    private final long mCount;
+    private final PersistableBundle mInfoParams;
+
+    /**
+     * Construct a token count using the count value and associated params.
+     */
+    public TokenInfo(long count, @NonNull PersistableBundle persistableBundle) {
+        this.mCount = count;
+        mInfoParams = persistableBundle;
+    }
+
+    /**
+     * Construct a token count using the count value.
+     */
+    public TokenInfo(long count) {
+        this.mCount = count;
+        this.mInfoParams = new PersistableBundle();
+    }
+
+    /**
+     * Returns the token count associated with a request payload.
+     */
+    public long getCount() {
+        return mCount;
+    }
+
+    /**
+     * Returns the params representing token info.
+     */
+    @NonNull
+    public PersistableBundle getInfoParams() {
+        return mInfoParams;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mCount);
+        dest.writePersistableBundle(mInfoParams);
+    }
+
+    public static final @NonNull Parcelable.Creator<TokenInfo> CREATOR
+            = new Parcelable.Creator<>() {
+        @Override
+        public TokenInfo[] newArray(int size) {
+            return new TokenInfo[size];
+        }
+
+        @Override
+        public TokenInfo createFromParcel(@NonNull Parcel in) {
+            return new TokenInfo(in.readLong(), in.readPersistableBundle());
+        }
+    };
+}
diff --git a/core/java/android/app/servertransaction/DestroyActivityItem.java b/core/java/android/app/servertransaction/DestroyActivityItem.java
index f9cf075..b0213d7 100644
--- a/core/java/android/app/servertransaction/DestroyActivityItem.java
+++ b/core/java/android/app/servertransaction/DestroyActivityItem.java
@@ -33,7 +33,6 @@
 public class DestroyActivityItem extends ActivityLifecycleItem {
 
     private boolean mFinished;
-    private int mConfigChanges;
 
     @Override
     public void preExecute(@NonNull ClientTransactionHandler client) {
@@ -44,7 +43,7 @@
     public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
             @NonNull PendingTransactionActions pendingActions) {
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy");
-        client.handleDestroyActivity(r, mFinished, mConfigChanges,
+        client.handleDestroyActivity(r, mFinished,
                 false /* getNonConfigInstance */, "DestroyActivityItem");
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
     }
@@ -67,15 +66,13 @@
 
     /** Obtain an instance initialized with provided params. */
     @NonNull
-    public static DestroyActivityItem obtain(@NonNull IBinder activityToken, boolean finished,
-            int configChanges) {
+    public static DestroyActivityItem obtain(@NonNull IBinder activityToken, boolean finished) {
         DestroyActivityItem instance = ObjectPool.obtain(DestroyActivityItem.class);
         if (instance == null) {
             instance = new DestroyActivityItem();
         }
         instance.setActivityToken(activityToken);
         instance.mFinished = finished;
-        instance.mConfigChanges = configChanges;
 
         return instance;
     }
@@ -84,7 +81,6 @@
     public void recycle() {
         super.recycle();
         mFinished = false;
-        mConfigChanges = 0;
         ObjectPool.recycle(this);
     }
 
@@ -95,14 +91,12 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
         dest.writeBoolean(mFinished);
-        dest.writeInt(mConfigChanges);
     }
 
     /** Read from Parcel. */
     private DestroyActivityItem(@NonNull Parcel in) {
         super(in);
         mFinished = in.readBoolean();
-        mConfigChanges = in.readInt();
     }
 
     public static final @NonNull Creator<DestroyActivityItem> CREATOR = new Creator<>() {
@@ -124,7 +118,7 @@
             return false;
         }
         final DestroyActivityItem other = (DestroyActivityItem) o;
-        return mFinished == other.mFinished && mConfigChanges == other.mConfigChanges;
+        return mFinished == other.mFinished;
     }
 
     @Override
@@ -132,14 +126,12 @@
         int result = 17;
         result = 31 * result + super.hashCode();
         result = 31 * result + (mFinished ? 1 : 0);
-        result = 31 * result + mConfigChanges;
         return result;
     }
 
     @Override
     public String toString() {
         return "DestroyActivityItem{" + super.toString()
-                + ",finished=" + mFinished
-                + ",mConfigChanges=" + mConfigChanges + "}";
+                + ",finished=" + mFinished + "}";
     }
 }
diff --git a/core/java/android/app/servertransaction/PauseActivityItem.java b/core/java/android/app/servertransaction/PauseActivityItem.java
index 8f1e90b..d230284 100644
--- a/core/java/android/app/servertransaction/PauseActivityItem.java
+++ b/core/java/android/app/servertransaction/PauseActivityItem.java
@@ -37,7 +37,6 @@
 
     private boolean mFinished;
     private boolean mUserLeaving;
-    private int mConfigChanges;
     private boolean mDontReport;
     private boolean mAutoEnteringPip;
 
@@ -45,7 +44,7 @@
     public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
             @NonNull PendingTransactionActions pendingActions) {
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
-        client.handlePauseActivity(r, mFinished, mUserLeaving, mConfigChanges, mAutoEnteringPip,
+        client.handlePauseActivity(r, mFinished, mUserLeaving, mAutoEnteringPip,
                 pendingActions, "PAUSE_ACTIVITY_ITEM");
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
     }
@@ -72,7 +71,7 @@
     /** Obtain an instance initialized with provided params. */
     @NonNull
     public static PauseActivityItem obtain(@NonNull IBinder activityToken, boolean finished,
-            boolean userLeaving, int configChanges, boolean dontReport, boolean autoEnteringPip) {
+            boolean userLeaving, boolean dontReport, boolean autoEnteringPip) {
         PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class);
         if (instance == null) {
             instance = new PauseActivityItem();
@@ -80,7 +79,6 @@
         instance.setActivityToken(activityToken);
         instance.mFinished = finished;
         instance.mUserLeaving = userLeaving;
-        instance.mConfigChanges = configChanges;
         instance.mDontReport = dontReport;
         instance.mAutoEnteringPip = autoEnteringPip;
 
@@ -91,7 +89,7 @@
     @NonNull
     public static PauseActivityItem obtain(@NonNull IBinder activityToken) {
         return obtain(activityToken, false /* finished */, false /* userLeaving */,
-                0 /* configChanges */, true /* dontReport */, false /* autoEnteringPip*/);
+                true /* dontReport */, false /* autoEnteringPip*/);
     }
 
     @Override
@@ -99,7 +97,6 @@
         super.recycle();
         mFinished = false;
         mUserLeaving = false;
-        mConfigChanges = 0;
         mDontReport = false;
         mAutoEnteringPip = false;
         ObjectPool.recycle(this);
@@ -113,7 +110,6 @@
         super.writeToParcel(dest, flags);
         dest.writeBoolean(mFinished);
         dest.writeBoolean(mUserLeaving);
-        dest.writeInt(mConfigChanges);
         dest.writeBoolean(mDontReport);
         dest.writeBoolean(mAutoEnteringPip);
     }
@@ -123,7 +119,6 @@
         super(in);
         mFinished = in.readBoolean();
         mUserLeaving = in.readBoolean();
-        mConfigChanges = in.readInt();
         mDontReport = in.readBoolean();
         mAutoEnteringPip = in.readBoolean();
     }
@@ -148,7 +143,7 @@
         }
         final PauseActivityItem other = (PauseActivityItem) o;
         return mFinished == other.mFinished && mUserLeaving == other.mUserLeaving
-                && mConfigChanges == other.mConfigChanges && mDontReport == other.mDontReport
+                && mDontReport == other.mDontReport
                 && mAutoEnteringPip == other.mAutoEnteringPip;
     }
 
@@ -158,7 +153,6 @@
         result = 31 * result + super.hashCode();
         result = 31 * result + (mFinished ? 1 : 0);
         result = 31 * result + (mUserLeaving ? 1 : 0);
-        result = 31 * result + mConfigChanges;
         result = 31 * result + (mDontReport ? 1 : 0);
         result = 31 * result + (mAutoEnteringPip ? 1 : 0);
         return result;
@@ -169,7 +163,6 @@
         return "PauseActivityItem{" + super.toString()
                 + ",finished=" + mFinished
                 + ",userLeaving=" + mUserLeaving
-                + ",configChanges=" + mConfigChanges
                 + ",dontReport=" + mDontReport
                 + ",autoEnteringPip=" + mAutoEnteringPip + "}";
     }
diff --git a/core/java/android/app/servertransaction/StopActivityItem.java b/core/java/android/app/servertransaction/StopActivityItem.java
index b8ce52d..def7b3f 100644
--- a/core/java/android/app/servertransaction/StopActivityItem.java
+++ b/core/java/android/app/servertransaction/StopActivityItem.java
@@ -19,7 +19,6 @@
 import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.ActivityThread.ActivityClientRecord;
 import android.app.ClientTransactionHandler;
 import android.os.IBinder;
@@ -34,13 +33,11 @@
 
     private static final String TAG = "StopActivityItem";
 
-    private int mConfigChanges;
-
     @Override
     public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
             @NonNull PendingTransactionActions pendingActions) {
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
-        client.handleStopActivity(r, mConfigChanges, pendingActions,
+        client.handleStopActivity(r, pendingActions,
                 true /* finalStateRequest */, "STOP_ACTIVITY_ITEM");
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
     }
@@ -63,16 +60,14 @@
     /**
      * Obtain an instance initialized with provided params.
      * @param activityToken the activity that stops.
-     * @param configChanges Configuration pieces that changed.
      */
     @NonNull
-    public static StopActivityItem obtain(@NonNull IBinder activityToken, int configChanges) {
+    public static StopActivityItem obtain(@NonNull IBinder activityToken) {
         StopActivityItem instance = ObjectPool.obtain(StopActivityItem.class);
         if (instance == null) {
             instance = new StopActivityItem();
         }
         instance.setActivityToken(activityToken);
-        instance.mConfigChanges = configChanges;
 
         return instance;
     }
@@ -80,23 +75,14 @@
     @Override
     public void recycle() {
         super.recycle();
-        mConfigChanges = 0;
         ObjectPool.recycle(this);
     }
 
     // Parcelable implementation
 
-    /** Write to Parcel. */
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        super.writeToParcel(dest, flags);
-        dest.writeInt(mConfigChanges);
-    }
-
     /** Read from Parcel. */
     private StopActivityItem(@NonNull Parcel in) {
         super(in);
-        mConfigChanges = in.readInt();
     }
 
     public static final @NonNull Creator<StopActivityItem> CREATOR = new Creator<>() {
@@ -110,28 +96,7 @@
     };
 
     @Override
-    public boolean equals(@Nullable Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!super.equals(o)) {
-            return false;
-        }
-        final StopActivityItem other = (StopActivityItem) o;
-        return mConfigChanges == other.mConfigChanges;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = 17;
-        result = 31 * result + super.hashCode();
-        result = 31 * result + mConfigChanges;
-        return result;
-    }
-
-    @Override
     public String toString() {
-        return "StopActivityItem{" + super.toString()
-                + ",configChanges=" + mConfigChanges + "}";
+        return "StopActivityItem{" + super.toString() + "}";
     }
 }
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index fa73c99..c837191 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -334,18 +334,18 @@
                     break;
                 case ON_PAUSE:
                     mTransactionHandler.handlePauseActivity(r, false /* finished */,
-                            false /* userLeaving */, 0 /* configChanges */,
+                            false /* userLeaving */,
                             false /* autoEnteringPip */, mPendingActions,
                             "LIFECYCLER_PAUSE_ACTIVITY");
                     break;
                 case ON_STOP:
-                    mTransactionHandler.handleStopActivity(r, 0 /* configChanges */,
+                    mTransactionHandler.handleStopActivity(r,
                             mPendingActions, false /* finalStateRequest */,
                             "LIFECYCLER_STOP_ACTIVITY");
                     break;
                 case ON_DESTROY:
                     mTransactionHandler.handleDestroyActivity(r, false /* finishing */,
-                            0 /* configChanges */, false /* getNonConfigInstance */,
+                            false /* getNonConfigInstance */,
                             "performLifecycleSequence. cycling to:" + path.get(size - 1));
                     break;
                 case ON_RESTART:
diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
index 475c6fb..710261a 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -200,7 +200,7 @@
                 lifecycleItem = PauseActivityItem.obtain(r.token);
                 break;
             case ON_STOP:
-                lifecycleItem = StopActivityItem.obtain(r.token, 0 /* configChanges */);
+                lifecycleItem = StopActivityItem.obtain(r.token);
                 break;
             default:
                 lifecycleItem = ResumeActivityItem.obtain(r.token, false /* isForward */,
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index eb82e1f..cda4d89 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -1417,13 +1417,15 @@
      * @see AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN
      * @see AppWidgetProviderInfo#WIDGET_CATEGORY_KEYGUARD
      * @see AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX
+     *
+     * @return true if the call was successful, false if it was rate-limited.
      */
     @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
-    public void setWidgetPreview(@NonNull ComponentName provider,
+    public boolean setWidgetPreview(@NonNull ComponentName provider,
             @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
             @NonNull RemoteViews preview) {
         try {
-            mService.setWidgetPreview(provider, widgetCategories, preview);
+            return mService.setWidgetPreview(provider, widgetCategories, preview);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 70d2c7a..c7e5d88 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5081,8 +5081,6 @@
      * @see #getSystemService
      * @see android.hardware.face.FaceManager
      */
-    @FlaggedApi(android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION)
-    @SystemApi
     public static final String FACE_SERVICE = "face";
 
     /**
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 6bb9c33..41c1f17 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -697,8 +697,9 @@
     public List<UserHandle> getProfiles() {
         if (mUserManager.isManagedProfile()
                 || (android.multiuser.Flags.enableLauncherAppsHiddenProfileChecks()
-                        && android.os.Flags.allowPrivateProfile()
-                        && mUserManager.isPrivateProfile())) {
+                    && android.os.Flags.allowPrivateProfile()
+                    && android.multiuser.Flags.enablePrivateSpaceFeatures()
+                    && mUserManager.isPrivateProfile())) {
             // If it's a managed or private profile, only return the current profile.
             final List result = new ArrayList(1);
             result.add(android.os.Process.myUserHandle());
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index cdda12e..3f941da 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -273,6 +273,10 @@
      * to the <code>retailDemo</code> value of
      * {@link android.R.attr#protectionLevel}.
      *
+     * @deprecated This flag has been replaced by the
+     *             {@link android.R.string#config_defaultRetailDemo retail demo role} and is a
+     *             no-op since {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}.
+     *
      * @hide
      */
     @SystemApi
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index ac80561..48a7cc9 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -184,3 +184,11 @@
     description: "Enable Private Space telephony and SMS intent redirection to the main user"
     bug: "325576602"
 }
+
+flag {
+    name: "block_private_space_creation"
+    namespace: "profile_experiences"
+    description: "Allow blocking private space creation based on specific conditions"
+    bug: "290333800"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
index 4dcc517..a908456 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
@@ -163,14 +163,31 @@
     }
 
     /**
-     * Update the URI relative filter groups for a package. All previously existing groups
-     * will be cleared before the new groups will be applied.
+     * Update the URI relative filter groups for a package. The groups set using this API acts
+     * as an additional filtering layer during intent resolution. It does not replace any
+     * existing groups that have been added to the package's intent filters either using the
+     * {@link android.content.IntentFilter#addUriRelativeFilterGroup(UriRelativeFilterGroup)}
+     * API or defined in the manifest.
+     * <p>
+     * Groups can be indexed to any domain or can be indexed for all subdomains by prefixing the
+     * hostname with a wildcard (i.e. "*.example.com"). Priority will be first given to groups
+     * that are indexed to the specific subdomain of the intent's data URI followed by any groups
+     * indexed to wildcard subdomains. If the subdomain consists of more than one label, priority
+     * will decrease corresponding to the decreasing number of subdomain labels after the wildcard.
+     * For example "a.b.c.d" will match "*.b.c.d" before "*.c.d".
+     * <p>
+     * All previously existing groups set for a domain index using this API will be cleared when
+     * new groups are set.
      *
      * @param packageName The name of the package.
      * @param domainToGroupsMap A map of domains to a list of {@link UriRelativeFilterGroup}s that
      *                         should apply to them. Groups for each domain will replace any groups
-     *                         provided for that domain in a prior call to this method. Groups will
+     *                         provided for that domain in a prior call to this method. To clear
+     *                         existing groups, set the list to null or a empty list. Groups will
      *                         be evaluated in the order they are provided.
+     *
+     * @see UriRelativeFilterGroup
+     * @see android.content.IntentFilter
      * @hide
      */
     @SystemApi
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index d259e97..273e40a 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -471,6 +471,16 @@
         return addAssetPathInternal(path, true /*overlay*/, false /*appAsLib*/);
     }
 
+    /**
+     * @hide
+     */
+    public void addSharedLibraryPaths(@NonNull String[] paths) {
+        final int length = paths.length;
+        for (int i = 0; i < length; i++) {
+            addAssetPathInternal(paths[i], false, true);
+        }
+    }
+
     private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
         Objects.requireNonNull(path, "path");
         synchronized (this) {
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 7fba3e8..1f5f88f 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -43,6 +43,7 @@
 import android.annotation.StyleableRes;
 import android.annotation.XmlRes;
 import android.app.Application;
+import android.app.ResourcesManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -2854,6 +2855,11 @@
     @FlaggedApi(android.content.res.Flags.FLAG_REGISTER_RESOURCE_PATHS)
     public static void registerResourcePaths(@NonNull String uniqueId,
             @NonNull ApplicationInfo appInfo) {
-        throw new UnsupportedOperationException("The implementation has not been done yet.");
+        if (Flags.registerResourcePaths()) {
+            ResourcesManager.getInstance().registerResourcePaths(uniqueId, appInfo);
+        } else {
+            throw new UnsupportedOperationException("Flag " + Flags.FLAG_REGISTER_RESOURCE_PATHS
+                    + " is disabled.");
+        }
     }
 }
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index 079c2c1..8d045aa 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -29,6 +29,7 @@
 import android.annotation.StyleableRes;
 import android.app.LocaleConfig;
 import android.app.ResourcesManager;
+import android.app.ResourcesManager.SharedLibraryAssets;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ActivityInfo.Config;
@@ -47,6 +48,7 @@
 import android.os.LocaleList;
 import android.os.ParcelFileDescriptor;
 import android.os.Trace;
+import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -197,6 +199,14 @@
     public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
             @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
         mAssets = assets;
+        if (Flags.registerResourcePaths()) {
+            ArrayMap<String, SharedLibraryAssets> sharedLibMap =
+                    ResourcesManager.getInstance().getSharedLibAssetsMap();
+            final int size = sharedLibMap.size();
+            for (int i = 0; i < size; i++) {
+                assets.addSharedLibraryPaths(sharedLibMap.valueAt(i).getAllAssetPaths());
+            }
+        }
         mMetrics.setToDefaults();
         mDisplayAdjustments = displayAdjustments;
         mConfiguration.setToDefaults();
diff --git a/core/java/android/hardware/ISensorPrivacyManager.aidl b/core/java/android/hardware/ISensorPrivacyManager.aidl
index 851ce2a..19d1029 100644
--- a/core/java/android/hardware/ISensorPrivacyManager.aidl
+++ b/core/java/android/hardware/ISensorPrivacyManager.aidl
@@ -16,7 +16,6 @@
 
 package android.hardware;
 
-import android.hardware.CameraPrivacyAllowlistEntry;
 import android.hardware.ISensorPrivacyListener;
 
 /** @hide */
@@ -48,7 +47,7 @@
     void setToggleSensorPrivacyForProfileGroup(int userId, int source, int sensor, boolean enable);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist();
+    List<String> getCameraPrivacyAllowlist();
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
     int getToggleSensorPrivacyState(int toggleType, int sensor);
@@ -62,6 +61,10 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
     boolean isCameraPrivacyEnabled(String packageName);
 
+    /** @hide */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY)")
+    void setCameraPrivacyAllowlist(in List<String> allowlist);
+
     // =============== End of transactions used on native side as well ============================
 
     void suppressToggleSensorPrivacyReminders(int userId, int sensor, IBinder token,
diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java
index 6294a8d..4cdaaddd 100644
--- a/core/java/android/hardware/SensorPrivacyManager.java
+++ b/core/java/android/hardware/SensorPrivacyManager.java
@@ -43,7 +43,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Map;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -204,6 +204,8 @@
      * Types of state which can exist for the sensor privacy toggle
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
     public static class StateTypes {
         private StateTypes() {}
 
@@ -217,30 +219,12 @@
          */
         public static final int DISABLED = SensorPrivacyIndividualEnabledSensorProto.DISABLED;
 
-        /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are helpful for driving.
-         */
-        @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS;
-
          /**
          * Constant indicating privacy is enabled except for the automotive driver assistance apps
          * which are required by car manufacturer for driving.
          */
-        @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS;
-
-        /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are both helpful for driving and also apps required by car manufacturer for
-         * driving.
-         */
-        @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_APPS;
+        public static final int ENABLED_EXCEPT_ALLOWLISTED_APPS =
+                SensorPrivacyIndividualEnabledSensorProto.ENABLED_EXCEPT_ALLOWLISTED_APPS;
 
         /**
          * Types of state which can exist for a sensor privacy toggle
@@ -250,9 +234,7 @@
         @IntDef(value = {
                 ENABLED,
                 DISABLED,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_APPS
+                ENABLED_EXCEPT_ALLOWLISTED_APPS
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface StateType {}
@@ -369,9 +351,6 @@
     private final ArrayMap<Pair<Integer, OnSensorPrivacyChangedListener>,
             OnSensorPrivacyChangedListener> mLegacyToggleListeners = new ArrayMap<>();
 
-    @GuardedBy("mLock")
-    private ArrayMap<String, Boolean> mCameraPrivacyAllowlist = null;
-
     /** The singleton ISensorPrivacyListener for IPC which will be used to dispatch to local
      * listeners */
     @NonNull
@@ -397,7 +376,8 @@
 
         @Override
         @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-        public void onSensorPrivacyStateChanged(int toggleType, int sensor, int state) {
+        public void onSensorPrivacyStateChanged(@ToggleType int toggleType,
+                @Sensors.Sensor int sensor, @StateTypes.StateType int state) {
             synchronized (mLock) {
                 for (int i = 0; i < mToggleListeners.size(); i++) {
                     OnSensorPrivacyChangedListener listener = mToggleListeners.keyAt(i);
@@ -725,6 +705,8 @@
     /**
      * Returns sensor privacy state for a specific sensor.
      *
+     * @param toggleType The type of toggle to use
+     * @param sensor The sensor to check
      * @return int sensor privacy state.
      *
      * @hide
@@ -741,10 +723,11 @@
         }
     }
 
-  /**
+   /**
      * Returns if camera privacy is enabled for a specific package.
      *
-     * @return boolean sensor privacy state.
+     * @param packageName The package to check
+     * @return boolean camera privacy state.
      *
      * @hide
      */
@@ -763,29 +746,41 @@
      * Returns camera privacy allowlist.
      *
      * @return List of automotive driver assistance packages for
-     * privacy allowlisting. The returned map includes the package
-     * name as key and the value is a Boolean which tells if that package
-     * is required by the car manufacturer as mandatory package for driving.
+     * privacy allowlisting.
      *
      * @hide
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
     @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-    public @NonNull Map<String, Boolean>  getCameraPrivacyAllowlist() {
+    public @NonNull List<String>  getCameraPrivacyAllowlist() {
         synchronized (mLock) {
-            if (mCameraPrivacyAllowlist == null) {
-                mCameraPrivacyAllowlist = new ArrayMap<>();
-                try {
-                    for (CameraPrivacyAllowlistEntry entry :
-                            mService.getCameraPrivacyAllowlist()) {
-                        mCameraPrivacyAllowlist.put(entry.packageName, entry.isMandatory);
-                    }
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
-                }
+            try {
+                return mService.getCameraPrivacyAllowlist();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
             }
-            return mCameraPrivacyAllowlist;
+        }
+    }
+
+    /**
+     * Sets camera privacy allowlist.
+     *
+     * @param allowlist List of automotive driver assistance packages for
+     * privacy allowlisting.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
+    @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
+    public void setCameraPrivacyAllowlist(@NonNull List<String> allowlist) {
+        synchronized (mLock) {
+            try {
+                mService.setCameraPrivacyAllowlist(allowlist);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
         }
     }
 
@@ -867,6 +862,7 @@
     /**
      * Sets sensor privacy to the specified state for an individual sensor.
      *
+     * @param source the source using which the sensor is toggled
      * @param sensor the sensor which to change the state for
      * @param state the state to which sensor privacy should be set.
      *
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index d9d4305..0208fed 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -23,7 +23,6 @@
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT;
 import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
-import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
 import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT;
 import static android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE;
 
@@ -1128,8 +1127,6 @@
          * @hide
          */
         @Override
-        @TestApi
-        @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
         public void onAuthenticationAcquired(int acquireInfo) {}
 
         /**
diff --git a/core/java/android/hardware/biometrics/SensorProperties.java b/core/java/android/hardware/biometrics/SensorProperties.java
index 16f71414..3b9cad4 100644
--- a/core/java/android/hardware/biometrics/SensorProperties.java
+++ b/core/java/android/hardware/biometrics/SensorProperties.java
@@ -16,9 +16,6 @@
 
 package android.hardware.biometrics;
 
-import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
-
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.TestApi;
@@ -144,10 +141,8 @@
     /**
      * @hide
      */
-    @TestApi
-    @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
     public SensorProperties(int sensorId, @Strength int sensorStrength,
-            @NonNull List<ComponentInfo> componentInfo) {
+            List<ComponentInfo> componentInfo) {
         mSensorId = sensorId;
         mSensorStrength = sensorStrength;
         mComponentInfo = componentInfo;
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 8165d44..3ba8be4 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -28,10 +28,3 @@
   bug: "302735104"
 }
 
-flag {
-  name: "face_background_authentication"
-  namespace: "biometrics_framework"
-  description: "Feature flag for allowing face background authentication with USE_BACKGROUND_FACE_AUTHENTICATION."
-  bug: "318584190"
-}
-
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 1b0a485..210ce2b 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -18,36 +18,28 @@
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.MANAGE_BIOMETRIC;
-import static android.Manifest.permission.TEST_BIOMETRIC;
-import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE;
-import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
 import android.annotation.SystemService;
-import android.annotation.TestApi;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFaceConstants;
-import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.BiometricStateListener;
-import android.hardware.biometrics.BiometricTestSession;
 import android.hardware.biometrics.CryptoObject;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
 import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.CancellationSignal.OnCancelListener;
 import android.os.Handler;
-import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.IRemoteCallback;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.Trace;
@@ -57,22 +49,15 @@
 import android.view.Surface;
 
 import com.android.internal.R;
+import com.android.internal.os.SomeArgs;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Executor;
 
 /**
  * A class that coordinates access to the face authentication hardware.
- *
- * <p>Please use {@link BiometricPrompt} for face authentication unless the experience must be
- * customized for unique system-level utilities, like the lock screen or ambient background usage.
- *
  * @hide
  */
-@FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
-@SystemApi
-@TestApi
 @SystemService(Context.FACE_SERVICE)
 public class FaceManager implements BiometricAuthenticator, BiometricFaceConstants {
 
@@ -103,76 +88,81 @@
     @Nullable private GenerateChallengeCallback mGenerateChallengeCallback;
     private CryptoObject mCryptoObject;
     private Face mRemovalFace;
-    private Executor mExecutor;
+    private Handler mHandler;
     private List<FaceSensorPropertiesInternal> mProps = new ArrayList<>();
 
     private final IFaceServiceReceiver mServiceReceiver = new IFaceServiceReceiver.Stub() {
 
         @Override // binder call
         public void onEnrollResult(Face face, int remaining) {
-            mExecutor.execute(() -> sendEnrollResult(face, remaining));
+            mHandler.obtainMessage(MSG_ENROLL_RESULT, remaining, 0, face).sendToTarget();
         }
 
         @Override // binder call
         public void onAcquired(int acquireInfo, int vendorCode) {
-            mExecutor.execute(() -> sendAcquiredResult(acquireInfo, vendorCode));
+            mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode).sendToTarget();
         }
 
         @Override // binder call
         public void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) {
-            mExecutor.execute(() -> sendAuthenticatedSucceeded(face, userId, isStrongBiometric));
+            mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId,
+                    isStrongBiometric ? 1 : 0, face).sendToTarget();
         }
 
         @Override // binder call
         public void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric) {
-            mExecutor.execute(() -> sendFaceDetected(sensorId, userId, isStrongBiometric));
+            mHandler.obtainMessage(MSG_FACE_DETECTED, sensorId, userId, isStrongBiometric)
+                    .sendToTarget();
         }
 
         @Override // binder call
         public void onAuthenticationFailed() {
-            mExecutor.execute(() -> sendAuthenticatedFailed());
+            mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
         }
 
         @Override // binder call
         public void onError(int error, int vendorCode) {
-            mExecutor.execute(() -> sendErrorResult(error, vendorCode));
+            mHandler.obtainMessage(MSG_ERROR, error, vendorCode).sendToTarget();
         }
 
         @Override // binder call
         public void onRemoved(Face face, int remaining) {
-            mExecutor.execute(() -> {
-                sendRemovedResult(face, remaining);
-                if (remaining == 0) {
-                    Settings.Secure.putIntForUser(mContext.getContentResolver(),
-                            Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
-                            UserHandle.USER_CURRENT);
-                }
-            });
+            mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget();
+            if (remaining == 0) {
+                Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                        Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
+                        UserHandle.USER_CURRENT);
+            }
         }
 
         @Override
         public void onFeatureSet(boolean success, int feature) {
-            mExecutor.execute(() -> sendSetFeatureCompleted(success, feature));
+            mHandler.obtainMessage(MSG_SET_FEATURE_COMPLETED, feature, 0, success).sendToTarget();
         }
 
         @Override
         public void onFeatureGet(boolean success, int[] features, boolean[] featureState) {
-            mExecutor.execute(() -> sendGetFeatureCompleted(success, features, featureState));
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = success;
+            args.arg2 = features;
+            args.arg3 = featureState;
+            mHandler.obtainMessage(MSG_GET_FEATURE_COMPLETED, args).sendToTarget();
         }
 
         @Override
         public void onChallengeGenerated(int sensorId, int userId, long challenge) {
-            mExecutor.execute(() -> sendChallengeGenerated(sensorId, userId, challenge));
+            mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge)
+                    .sendToTarget();
         }
 
         @Override
         public void onAuthenticationFrame(FaceAuthenticationFrame frame) {
-            mExecutor.execute(() -> sendAuthenticationFrame(frame));
+            mHandler.obtainMessage(MSG_AUTHENTICATION_FRAME, frame).sendToTarget();
         }
 
         @Override
         public void onEnrollmentFrame(FaceEnrollFrame frame) {
-            mExecutor.execute(() -> sendEnrollmentFrame(frame));
+            mHandler.obtainMessage(MSG_ENROLLMENT_FRAME, frame).sendToTarget();
         }
     };
 
@@ -185,7 +175,7 @@
         if (mService == null) {
             Slog.v(TAG, "FaceAuthenticationManagerService was null");
         }
-        mExecutor = context.getMainExecutor();
+        mHandler = new MyHandler(context);
         if (context.checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL)
                 == PackageManager.PERMISSION_GRANTED) {
             addAuthenticatorsRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
@@ -199,16 +189,18 @@
     }
 
     /**
-     * Returns an {@link Executor} for the given {@link Handler} or the main {@link Executor} if
-     * {@code handler} is {@code null}.
+     * Use the provided handler thread for events.
      */
-    private @NonNull Executor createExecutorForHandlerIfNeeded(@Nullable Handler handler) {
-        return handler != null ? new HandlerExecutor(handler) : mContext.getMainExecutor();
+    private void useHandler(Handler handler) {
+        if (handler != null) {
+            mHandler = new MyHandler(handler.getLooper());
+        } else if (mHandler.getLooper() != mContext.getMainLooper()) {
+            mHandler = new MyHandler(mContext.getMainLooper());
+        }
     }
 
     /**
      * @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FaceAuthenticateOptions)}.
-     * @hide
      */
     @Deprecated
     @RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -220,22 +212,17 @@
     }
 
     /**
-     * Request authentication.
-     *
-     * <p>This call operates the face recognition hardware and starts capturing images.
+     * Request authentication. This call operates the face recognition hardware and starts capturing images.
      * It terminates when
      * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
      * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
      * which point the object is no longer valid. The operation can be canceled by using the
-     * provided {@code cancel} object.
+     * provided cancel object.
      *
-     * @param crypto   the cryptographic operations to use for authentication or {@code null} if
-     *                 none required
-     * @param cancel   an object that can be used to cancel authentication or {@code null} if not
-     *                 needed
+     * @param crypto   object associated with the call or null if none required
+     * @param cancel   an object that can be used to cancel authentication
      * @param callback an object to receive authentication events
-     * @param handler  an optional handler to handle callback events or {@code null} to obtain main
-     *                 {@link Executor} from {@link Context}
+     * @param handler  an optional handler to handle callback events
      * @param options  additional options to customize this request
      * @throws IllegalArgumentException if the crypto operation is not supported or is not backed
      *                                  by
@@ -248,14 +235,6 @@
     public void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,
             @NonNull AuthenticationCallback callback, @Nullable Handler handler,
             @NonNull FaceAuthenticateOptions options) {
-        authenticate(crypto, cancel, callback, createExecutorForHandlerIfNeeded(handler),
-                options, false /* allowBackgroundAuthentication */);
-    }
-
-    @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION})
-    private void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,
-            @NonNull AuthenticationCallback callback, @NonNull Executor executor,
-            @NonNull FaceAuthenticateOptions options, boolean allowBackgroundAuthentication) {
         if (callback == null) {
             throw new IllegalArgumentException("Must supply an authentication callback");
         }
@@ -270,15 +249,13 @@
 
         if (mService != null) {
             try {
-                mExecutor = executor;
+                useHandler(handler);
                 mAuthenticationCallback = callback;
                 mCryptoObject = crypto;
                 final long operationId = crypto != null ? crypto.getOpId() : 0;
                 Trace.beginSection("FaceManager#authenticate");
-                final long authId = allowBackgroundAuthentication
-                        ? mService.authenticateInBackground(
-                                mToken, operationId, mServiceReceiver, options)
-                        : mService.authenticate(mToken, operationId, mServiceReceiver, options);
+                final long authId = mService.authenticate(
+                        mToken, operationId, mServiceReceiver, options);
                 if (cancel != null) {
                     cancel.setOnCancelListener(new OnAuthenticationCancelListener(authId));
                 }
@@ -296,67 +273,6 @@
     }
 
     /**
-     * Request background face authentication.
-     *
-     * <p>This call operates the face recognition hardware and starts capturing images.
-     * It terminates when
-     * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
-     * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationSucceeded(
-     * BiometricPrompt.AuthenticationResult)} is called, at which point the object is no longer
-     * valid. The operation can be canceled by using the provided cancel object.
-     *
-     * <p>See {@link BiometricPrompt#authenticate} for more details. Please use
-     * {@link BiometricPrompt} for face authentication unless the experience must be customized for
-     * unique system-level utilities, like the lock screen or ambient background usage.
-     *
-     * @param executor the specified {@link Executor} to handle callback events; if {@code null},
-     *                 the callback will be executed on the main {@link Executor}.
-     * @param crypto   the cryptographic operations to use for authentication or {@code null} if
-     *                 none required.
-     * @param cancel   an object that can be used to cancel authentication or {@code null} if not
-     *                 needed.
-     * @param callback an object to receive authentication events.
-     * @throws IllegalArgumentException if the crypto operation is not supported or is not backed
-     *                                  by
-     *                                  <a href="{@docRoot}training/articles/keystore.html">Android
-     *                                  Keystore facility</a>.
-     * @hide
-     */
-    @RequiresPermission(USE_BACKGROUND_FACE_AUTHENTICATION)
-    @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
-    @SystemApi
-    public void authenticateInBackground(@Nullable Executor executor,
-            @Nullable BiometricPrompt.CryptoObject crypto, @Nullable CancellationSignal cancel,
-            @NonNull BiometricPrompt.AuthenticationCallback callback) {
-        authenticate(crypto, cancel, new AuthenticationCallback() {
-                    @Override
-                    public void onAuthenticationError(int errorCode, CharSequence errString) {
-                        callback.onAuthenticationError(errorCode, errString);
-                    }
-
-                    @Override
-                    public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
-                        callback.onAuthenticationHelp(helpCode, helpString);
-                    }
-
-                    @Override
-                    public void onAuthenticationSucceeded(AuthenticationResult result) {
-                        callback.onAuthenticationSucceeded(
-                                new BiometricPrompt.AuthenticationResult(
-                                        crypto,
-                                        BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC));
-                    }
-
-                    @Override
-                    public void onAuthenticationFailed() {
-                        callback.onAuthenticationFailed();
-                    }
-                }, executor == null ? mContext.getMainExecutor() : executor,
-                new FaceAuthenticateOptions.Builder().build(),
-                true /* allowBackgroundAuthentication */);
-    }
-
-    /**
      * Uses the face hardware to detect for the presence of a face, without giving details about
      * accept/reject/lockout.
      * @hide
@@ -714,14 +630,12 @@
     }
 
     /**
-     * Determine if there are enrolled {@link Face} templates.
+     * Determine if there is a face enrolled.
      *
-     * @return {@code true} if there are enrolled {@link Face} templates, {@code false} otherwise
+     * @return true if a face is enrolled, false otherwise
      * @hide
      */
-    @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL, USE_BACKGROUND_FACE_AUTHENTICATION})
-    @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
-    @SystemApi
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
     public boolean hasEnrolledTemplates() {
         return hasEnrolledTemplates(UserHandle.myUserId());
     }
@@ -784,8 +698,6 @@
      * @hide
      */
     @NonNull
-    @TestApi
-    @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
     public List<FaceSensorProperties> getSensorProperties() {
         final List<FaceSensorProperties> properties = new ArrayList<>();
         final List<FaceSensorPropertiesInternal> internalProperties
@@ -888,7 +800,7 @@
                                             PowerManager.PARTIAL_WAKE_LOCK,
                                             "faceLockoutResetCallback");
                                     wakeLock.acquire();
-                                    mExecutor.execute(() -> {
+                                    mHandler.post(() -> {
                                         try {
                                             callback.onLockoutReset(sensorId);
                                         } finally {
@@ -1358,6 +1270,70 @@
         }
     }
 
+    private class MyHandler extends Handler {
+        private MyHandler(Context context) {
+            super(context.getMainLooper());
+        }
+
+        private MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(android.os.Message msg) {
+            Trace.beginSection("FaceManager#handleMessage: " + Integer.toString(msg.what));
+            switch (msg.what) {
+                case MSG_ENROLL_RESULT:
+                    sendEnrollResult((Face) msg.obj, msg.arg1 /* remaining */);
+                    break;
+                case MSG_ACQUIRED:
+                    sendAcquiredResult(msg.arg1 /* acquire info */, msg.arg2 /* vendorCode */);
+                    break;
+                case MSG_AUTHENTICATION_SUCCEEDED:
+                    sendAuthenticatedSucceeded((Face) msg.obj, msg.arg1 /* userId */,
+                            msg.arg2 == 1 /* isStrongBiometric */);
+                    break;
+                case MSG_AUTHENTICATION_FAILED:
+                    sendAuthenticatedFailed();
+                    break;
+                case MSG_ERROR:
+                    sendErrorResult(msg.arg1 /* errMsgId */, msg.arg2 /* vendorCode */);
+                    break;
+                case MSG_REMOVED:
+                    sendRemovedResult((Face) msg.obj, msg.arg1 /* remaining */);
+                    break;
+                case MSG_SET_FEATURE_COMPLETED:
+                    sendSetFeatureCompleted((boolean) msg.obj /* success */,
+                            msg.arg1 /* feature */);
+                    break;
+                case MSG_GET_FEATURE_COMPLETED:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    sendGetFeatureCompleted((boolean) args.arg1 /* success */,
+                            (int[]) args.arg2 /* features */,
+                            (boolean[]) args.arg3 /* featureState */);
+                    args.recycle();
+                    break;
+                case MSG_CHALLENGE_GENERATED:
+                    sendChallengeGenerated(msg.arg1 /* sensorId */, msg.arg2 /* userId */,
+                            (long) msg.obj /* challenge */);
+                    break;
+                case MSG_FACE_DETECTED:
+                    sendFaceDetected(msg.arg1 /* sensorId */, msg.arg2 /* userId */,
+                            (boolean) msg.obj /* isStrongBiometric */);
+                    break;
+                case MSG_AUTHENTICATION_FRAME:
+                    sendAuthenticationFrame((FaceAuthenticationFrame) msg.obj /* frame */);
+                    break;
+                case MSG_ENROLLMENT_FRAME:
+                    sendEnrollmentFrame((FaceEnrollFrame) msg.obj /* frame */);
+                    break;
+                default:
+                    Slog.w(TAG, "Unknown message: " + msg.what);
+            }
+            Trace.endSection();
+        }
+    }
+
     private void sendSetFeatureCompleted(boolean success, int feature) {
         if (mSetFeatureCallback == null) {
             return;
@@ -1634,23 +1610,4 @@
         Slog.w(TAG, "Unknown enrollment acquired message: " + acquireInfo + ", " + vendorCode);
         return null;
     }
-
-    /**
-     * Retrieves a test session for FaceManager.
-     *
-     * @hide
-     */
-    @TestApi
-    @NonNull
-    @RequiresPermission(TEST_BIOMETRIC)
-    @FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
-    public BiometricTestSession createTestSession(int sensorId) {
-        try {
-            return new BiometricTestSession(mContext, sensorId,
-                    (context, sensorId1, callback) -> mService
-                            .createTestSession(sensorId1, callback, context.getOpPackageName()));
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-}
\ No newline at end of file
+}
diff --git a/core/java/android/hardware/face/FaceSensorProperties.java b/core/java/android/hardware/face/FaceSensorProperties.java
index a1ddb0e..f613127 100644
--- a/core/java/android/hardware/face/FaceSensorProperties.java
+++ b/core/java/android/hardware/face/FaceSensorProperties.java
@@ -16,12 +16,8 @@
 
 package android.hardware.face;
 
-import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
-
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.TestApi;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.SensorProperties;
 
@@ -34,8 +30,6 @@
  * Container for face sensor properties.
  * @hide
  */
-@TestApi
-@FlaggedApi(FLAG_FACE_BACKGROUND_AUTHENTICATION)
 public class FaceSensorProperties extends SensorProperties {
     /**
      * @hide
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 6515312..553d9f7 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -39,7 +39,7 @@
 interface IFaceService {
 
     // Creates a test session with the specified sensorId
-    @EnforcePermission("TEST_BIOMETRIC")
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     ITestSession createTestSession(int sensorId, ITestSessionCallback callback, String opPackageName);
 
     // Requests a proto dump of the specified sensor
@@ -47,7 +47,7 @@
     byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer);
 
     // Retrieve static sensor properties for all face sensors
-    @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"})
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(String opPackageName);
 
     // Retrieve static sensor properties for the specified sensor
@@ -59,11 +59,6 @@
     long authenticate(IBinder token, long operationId, IFaceServiceReceiver receiver,
             in FaceAuthenticateOptions options);
 
-    // Authenticate with a face. A requestId is returned that can be used to cancel this operation.
-    @EnforcePermission("USE_BACKGROUND_FACE_AUTHENTICATION")
-    long authenticateInBackground(IBinder token, long operationId, IFaceServiceReceiver receiver,
-            in FaceAuthenticateOptions options);
-
     // Uses the face hardware to detect for the presence of a face, without giving details
     // about accept/reject/lockout. A requestId is returned that can be used to cancel this
     // operation.
@@ -138,7 +133,7 @@
     void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName, long challenge);
 
     // Determine if a user has at least one enrolled face
-    @EnforcePermission(anyOf = {"USE_BIOMETRIC_INTERNAL", "USE_BACKGROUND_FACE_AUTHENTICATION"})
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName);
 
     // Return the LockoutTracker status for the specified user
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 1f54959..2816f77 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -27,6 +27,7 @@
 import android.hardware.input.IKeyboardBacklightState;
 import android.hardware.input.IStickyModifierStateListener;
 import android.hardware.input.ITabletModeChangedListener;
+import android.hardware.input.KeyboardLayoutSelectionResult;
 import android.hardware.input.TouchCalibration;
 import android.os.CombinedVibration;
 import android.hardware.input.IInputSensorEventListener;
@@ -120,8 +121,9 @@
             String keyboardLayoutDescriptor);
 
     // New Keyboard layout config APIs
-    String getKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier, int userId,
-            in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype);
+    KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice(
+            in InputDeviceIdentifier identifier, int userId, in InputMethodInfo imeInfo,
+            in InputMethodSubtype imeSubtype);
 
     @EnforcePermission("SET_KEYBOARD_LAYOUT")
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 744dfae9..a1242fb 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -784,10 +784,10 @@
      *
      * @hide
      */
-    @Nullable
-    public String getKeyboardLayoutForInputDevice(@NonNull InputDeviceIdentifier identifier,
-            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
-            @Nullable InputMethodSubtype imeSubtype) {
+    @NonNull
+    public KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice(
+            @NonNull InputDeviceIdentifier identifier, @UserIdInt int userId,
+            @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) {
         try {
             return mIm.getKeyboardLayoutForInputDevice(identifier, userId, imeInfo, imeSubtype);
         } catch (RemoteException ex) {
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/hardware/input/KeyboardLayoutSelectionResult.aidl
similarity index 67%
copy from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
copy to core/java/android/hardware/input/KeyboardLayoutSelectionResult.aidl
index 838e41e..13be2ff 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/hardware/input/KeyboardLayoutSelectionResult.aidl
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2024, The Android Open Source Project
+/*
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -14,11 +14,6 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.hardware.input;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
-}
-
+parcelable KeyboardLayoutSelectionResult;
diff --git a/core/java/android/hardware/input/KeyboardLayoutSelectionResult.java b/core/java/android/hardware/input/KeyboardLayoutSelectionResult.java
new file mode 100644
index 0000000..5a1c947
--- /dev/null
+++ b/core/java/android/hardware/input/KeyboardLayoutSelectionResult.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcelable;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides information about the selected layout and the selection criteria when the caller calls
+ * {@link InputManager#getKeyboardLayoutForInputDevice(InputDeviceIdentifier, int, InputMethodInfo,
+ * InputMethodSubtype)}
+ *
+ * @hide
+ */
+
+@DataClass(genParcelable = true, genToString = true, genEqualsHashCode = true)
+public final class KeyboardLayoutSelectionResult implements Parcelable {
+    @Nullable
+    private final String mLayoutDescriptor;
+
+    /** Unspecified layout selection criteria */
+    public static final int LAYOUT_SELECTION_CRITERIA_UNSPECIFIED = 0;
+
+    /** Manual selection by user */
+    public static final int LAYOUT_SELECTION_CRITERIA_USER = 1;
+
+    /** Auto-detection based on device provided language tag and layout type */
+    public static final int LAYOUT_SELECTION_CRITERIA_DEVICE = 2;
+
+    /** Auto-detection based on IME provided language tag and layout type */
+    public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 3;
+
+    /** Default selection */
+    public static final int LAYOUT_SELECTION_CRITERIA_DEFAULT = 4;
+
+    /** Failed layout selection */
+    public static final KeyboardLayoutSelectionResult FAILED = new KeyboardLayoutSelectionResult(
+            null, LAYOUT_SELECTION_CRITERIA_UNSPECIFIED);
+
+    @LayoutSelectionCriteria
+    private final int mSelectionCriteria;
+
+
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/hardware/input/KeyboardLayoutSelectionResult.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    @IntDef(prefix = "LAYOUT_SELECTION_CRITERIA_", value = {
+        LAYOUT_SELECTION_CRITERIA_UNSPECIFIED,
+        LAYOUT_SELECTION_CRITERIA_USER,
+        LAYOUT_SELECTION_CRITERIA_DEVICE,
+        LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
+        LAYOUT_SELECTION_CRITERIA_DEFAULT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @DataClass.Generated.Member
+    public @interface LayoutSelectionCriteria {}
+
+    @DataClass.Generated.Member
+    public static String layoutSelectionCriteriaToString(@LayoutSelectionCriteria int value) {
+        switch (value) {
+            case LAYOUT_SELECTION_CRITERIA_UNSPECIFIED:
+                    return "LAYOUT_SELECTION_CRITERIA_UNSPECIFIED";
+            case LAYOUT_SELECTION_CRITERIA_USER:
+                    return "LAYOUT_SELECTION_CRITERIA_USER";
+            case LAYOUT_SELECTION_CRITERIA_DEVICE:
+                    return "LAYOUT_SELECTION_CRITERIA_DEVICE";
+            case LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD:
+                    return "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD";
+            case LAYOUT_SELECTION_CRITERIA_DEFAULT:
+                    return "LAYOUT_SELECTION_CRITERIA_DEFAULT";
+            default: return Integer.toHexString(value);
+        }
+    }
+
+    @DataClass.Generated.Member
+    public KeyboardLayoutSelectionResult(
+            @Nullable String layoutDescriptor,
+            @LayoutSelectionCriteria int selectionCriteria) {
+        this.mLayoutDescriptor = layoutDescriptor;
+        this.mSelectionCriteria = selectionCriteria;
+
+        if (!(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_UNSPECIFIED)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_USER)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEFAULT)) {
+            throw new java.lang.IllegalArgumentException(
+                    "selectionCriteria was " + mSelectionCriteria + " but must be one of: "
+                            + "LAYOUT_SELECTION_CRITERIA_UNSPECIFIED(" + LAYOUT_SELECTION_CRITERIA_UNSPECIFIED + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_USER(" + LAYOUT_SELECTION_CRITERIA_USER + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_DEVICE(" + LAYOUT_SELECTION_CRITERIA_DEVICE + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD(" + LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_DEFAULT(" + LAYOUT_SELECTION_CRITERIA_DEFAULT + ")");
+        }
+
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable String getLayoutDescriptor() {
+        return mLayoutDescriptor;
+    }
+
+    @DataClass.Generated.Member
+    public @LayoutSelectionCriteria int getSelectionCriteria() {
+        return mSelectionCriteria;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "KeyboardLayoutSelectionResult { " +
+                "layoutDescriptor = " + mLayoutDescriptor + ", " +
+                "selectionCriteria = " + layoutSelectionCriteriaToString(mSelectionCriteria) +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(KeyboardLayoutSelectionResult other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        KeyboardLayoutSelectionResult that = (KeyboardLayoutSelectionResult) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && java.util.Objects.equals(mLayoutDescriptor, that.mLayoutDescriptor)
+                && mSelectionCriteria == that.mSelectionCriteria;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mLayoutDescriptor);
+        _hash = 31 * _hash + mSelectionCriteria;
+        return _hash;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        byte flg = 0;
+        if (mLayoutDescriptor != null) flg |= 0x1;
+        dest.writeByte(flg);
+        if (mLayoutDescriptor != null) dest.writeString(mLayoutDescriptor);
+        dest.writeInt(mSelectionCriteria);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ KeyboardLayoutSelectionResult(@NonNull android.os.Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        byte flg = in.readByte();
+        String layoutDescriptor = (flg & 0x1) == 0 ? null : in.readString();
+        int selectionCriteria = in.readInt();
+
+        this.mLayoutDescriptor = layoutDescriptor;
+        this.mSelectionCriteria = selectionCriteria;
+
+        if (!(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_UNSPECIFIED)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_USER)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD)
+                && !(mSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEFAULT)) {
+            throw new java.lang.IllegalArgumentException(
+                    "selectionCriteria was " + mSelectionCriteria + " but must be one of: "
+                            + "LAYOUT_SELECTION_CRITERIA_UNSPECIFIED(" + LAYOUT_SELECTION_CRITERIA_UNSPECIFIED + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_USER(" + LAYOUT_SELECTION_CRITERIA_USER + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_DEVICE(" + LAYOUT_SELECTION_CRITERIA_DEVICE + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD(" + LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + "), "
+                            + "LAYOUT_SELECTION_CRITERIA_DEFAULT(" + LAYOUT_SELECTION_CRITERIA_DEFAULT + ")");
+        }
+
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<KeyboardLayoutSelectionResult> CREATOR
+            = new Parcelable.Creator<KeyboardLayoutSelectionResult>() {
+        @Override
+        public KeyboardLayoutSelectionResult[] newArray(int size) {
+            return new KeyboardLayoutSelectionResult[size];
+        }
+
+        @Override
+        public KeyboardLayoutSelectionResult createFromParcel(@NonNull android.os.Parcel in) {
+            return new KeyboardLayoutSelectionResult(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1709568115865L,
+            codegenVersion = "1.0.23",
+            sourceFile = "frameworks/base/core/java/android/hardware/input/KeyboardLayoutSelectionResult.java",
+            inputSignatures = "private final @android.annotation.Nullable java.lang.String mLayoutDescriptor\npublic static final  int LAYOUT_SELECTION_CRITERIA_UNSPECIFIED\npublic static final  int LAYOUT_SELECTION_CRITERIA_USER\npublic static final  int LAYOUT_SELECTION_CRITERIA_DEVICE\npublic static final  int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD\npublic static final  int LAYOUT_SELECTION_CRITERIA_DEFAULT\npublic static final  android.hardware.input.KeyboardLayoutSelectionResult FAILED\nprivate final @android.hardware.input.KeyboardLayoutSelectionResult.LayoutSelectionCriteria int mSelectionCriteria\nclass KeyboardLayoutSelectionResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genToString=true, genEqualsHashCode=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 800ba6d..23d6007 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -85,6 +85,7 @@
     boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, int mUserId);
     boolean isRestricted(int userId);
     boolean canHaveRestrictedProfile(int userId);
+    boolean canAddPrivateProfile(int userId);
     int getUserSerialNumber(int userId);
     int getUserHandle(int userSerialNumber);
     int getUserRestrictionSource(String restrictionKey, int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 9757a10..fdaa0b4 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2353,6 +2353,17 @@
     public static final int USER_OPERATION_ERROR_USER_ACCOUNT_ALREADY_EXISTS = 7;
 
     /**
+     * Indicates user operation failed because user is disabled on the device.
+     * @hide
+     */
+    public static final int USER_OPERATION_ERROR_DISABLED_USER = 8;
+    /**
+     * Indicates user operation failed because user is disabled on the device.
+     * @hide
+     */
+    public static final int USER_OPERATION_ERROR_PRIVATE_PROFILE = 9;
+
+    /**
      * Result returned from various user operations.
      *
      * @hide
@@ -2366,7 +2377,9 @@
             USER_OPERATION_ERROR_CURRENT_USER,
             USER_OPERATION_ERROR_LOW_STORAGE,
             USER_OPERATION_ERROR_MAX_USERS,
-            USER_OPERATION_ERROR_USER_ACCOUNT_ALREADY_EXISTS
+            USER_OPERATION_ERROR_USER_ACCOUNT_ALREADY_EXISTS,
+            USER_OPERATION_ERROR_DISABLED_USER,
+            USER_OPERATION_ERROR_PRIVATE_PROFILE,
     })
     public @interface UserOperationResult {}
 
@@ -2563,6 +2576,17 @@
     }
 
     /**
+     * Returns whether the device supports Private Profile
+     * @hide
+     */
+    public static boolean isPrivateProfileEnabled() {
+        if (android.multiuser.Flags.blockPrivateSpaceCreation()) {
+            return !ActivityManager.isLowRamDeviceStatic();
+        }
+        return true;
+    }
+
+    /**
      * Returns whether multiple admins are enabled on the device
      * @hide
      */
@@ -3155,6 +3179,27 @@
     }
 
     /**
+     * Checks if it's possible to add a private profile to the context user
+     * @return whether the context user can add a private profile.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS},
+            conditional = true)
+    @UserHandleAware
+    public boolean canAddPrivateProfile() {
+        if (android.multiuser.Flags.blockPrivateSpaceCreation()) {
+            try {
+                return mService.canAddPrivateProfile(mUserId);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+        }
+        return true;
+    }
+
+    /**
      * Returns whether the context user has at least one restricted profile associated with it.
      * @return whether the user has a restricted profile associated with it
      * @hide
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index a14a2c7..555a120 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -70,6 +70,8 @@
 
     private final boolean mDefaultKeyboardVibrationEnabled;
 
+    private final boolean mHasFixedKeyboardAmplitude;
+
     /** @hide */
     public VibrationConfig(@Nullable Resources resources) {
         mHapticChannelMaxVibrationAmplitude = loadFloat(resources,
@@ -87,6 +89,8 @@
                 com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false);
         mDefaultKeyboardVibrationEnabled = loadBoolean(resources,
                 com.android.internal.R.bool.config_defaultKeyboardVibrationEnabled, true);
+        mHasFixedKeyboardAmplitude = loadFloat(resources,
+                com.android.internal.R.dimen.config_keyboardHapticFeedbackFixedAmplitude, -1) > 0;
 
         mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources,
                 com.android.internal.R.integer.config_defaultAlarmVibrationIntensity);
@@ -197,6 +201,14 @@
         return mDefaultKeyboardVibrationEnabled;
     }
 
+    /**
+     * Whether the device has a fixed amplitude for keyboard.
+     * @hide
+     */
+    public boolean hasFixedKeyboardAmplitude() {
+        return mHasFixedKeyboardAmplitude;
+    }
+
     /** Get the default vibration intensity for given usage. */
     @VibrationIntensity
     public int getDefaultVibrationIntensity(@VibrationAttributes.Usage int usage) {
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 410f510..3c7692d 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1800,7 +1800,11 @@
      */
     @SystemApi
     @NonNull
-    @RequiresPermission(android.Manifest.permission.GET_RUNTIME_PERMISSIONS)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+            android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
+            android.Manifest.permission.GET_RUNTIME_PERMISSIONS
+    })
     @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
     public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName,
             @NonNull String persistentDeviceId) {
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index de7008b..dc782d4 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -138,3 +138,11 @@
     bug: "325356776"
 }
 
+flag {
+    name: "runtime_permission_appops_mapping_enabled"
+    is_fixed_read_only: true
+    namespace: "permissions"
+    description: "Use runtime permission state to determine appop state"
+    bug: "266164193"
+}
+
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index c03dc71..120846c 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -667,7 +667,8 @@
             @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
             public @NonNull AddCallParametersBuilder setAssertedDisplayName(
                     String assertedDisplayName) {
-                if (assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) {
+                if (assertedDisplayName != null
+                        && assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) {
                     throw new IllegalArgumentException("assertedDisplayName exceeds the character"
                             + " limit of " + MAX_NUMBER_OF_CHARACTERS + ".");
                 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index eea6464..e26dc73 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3583,6 +3583,12 @@
                                                     + " and user:" + userHandle
                                                     + " with index:" + index);
                                         }
+                                        // Always make sure to close any pre-existing tracker before
+                                        // replacing it, to prevent memory leaks
+                                        var oldTracker = mGenerationTrackers.get(name);
+                                        if (oldTracker != null) {
+                                            oldTracker.destroy();
+                                        }
                                         mGenerationTrackers.put(name, new GenerationTracker(name,
                                                 array, index, generation,
                                                 mGenerationTrackerErrorHandler));
@@ -3805,6 +3811,12 @@
                                         + " in package:" + cr.getPackageName()
                                         + " with index:" + index);
                             }
+                            // Always make sure to close any pre-existing tracker before
+                            // replacing it, to prevent memory leaks
+                            var oldTracker = mGenerationTrackers.get(prefix);
+                            if (oldTracker != null) {
+                                oldTracker.destroy();
+                            }
                             mGenerationTrackers.put(prefix,
                                     new GenerationTracker(prefix, array, index, generation,
                                             mGenerationTrackerErrorHandler));
@@ -19992,6 +20004,13 @@
             @Readable
             public static final String CONSISTENT_NOTIFICATION_BLOCKING_ENABLED =
                     "consistent_notification_blocking_enabled";
+
+            /**
+             * Whether the Auto Bedtime Mode experience is enabled.
+             *
+             * @hide
+             */
+            public static final String AUTO_BEDTIME_MODE = "auto_bedtime_mode";
         }
     }
 
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 7658af5..bd9ab86 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SystemApi;
+import android.annotation.UiThread;
 import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.app.Notification;
@@ -468,6 +469,7 @@
      *            object as well as its identifying information (tag and id) and source
      *            (package name).
      */
+    @UiThread
     public void onNotificationPosted(StatusBarNotification sbn) {
         // optional
     }
@@ -481,6 +483,7 @@
      * @param rankingMap The current ranking map that can be used to retrieve ranking information
      *                   for active notifications, including the newly posted one.
      */
+    @UiThread
     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
         onNotificationPosted(sbn);
     }
@@ -499,6 +502,7 @@
      *            and source (package name) used to post the {@link android.app.Notification} that
      *            was just removed.
      */
+    @UiThread
     public void onNotificationRemoved(StatusBarNotification sbn) {
         // optional
     }
@@ -520,6 +524,7 @@
      *                   for active notifications.
      *
      */
+    @UiThread
     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
         onNotificationRemoved(sbn);
     }
@@ -541,6 +546,7 @@
      * @param rankingMap The current ranking map that can be used to retrieve ranking information
      *                   for active notifications.
      */
+    @UiThread
     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
             @NotificationCancelReason int reason) {
         onNotificationRemoved(sbn, rankingMap);
@@ -552,6 +558,7 @@
      *
      * @hide
      */
+    @UiThread
     @SystemApi
     public void onNotificationRemoved(@NonNull StatusBarNotification sbn,
             @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason) {
@@ -563,6 +570,7 @@
      * the notification manager.  You are safe to call {@link #getActiveNotifications()}
      * at this time.
      */
+    @UiThread
     public void onListenerConnected() {
         // optional
     }
@@ -572,6 +580,7 @@
      * notification manager.You will not receive any events after this call, and may only
      * call {@link #requestRebind(ComponentName)} at this time.
      */
+    @UiThread
     public void onListenerDisconnected() {
         // optional
     }
@@ -582,6 +591,7 @@
      * @param rankingMap The current ranking map that can be used to retrieve ranking information
      *                   for active notifications.
      */
+    @UiThread
     public void onNotificationRankingUpdate(RankingMap rankingMap) {
         // optional
     }
@@ -592,6 +602,7 @@
      *
      * @param hints The current {@link #getCurrentListenerHints() listener hints}.
      */
+    @UiThread
     public void onListenerHintsChanged(int hints) {
         // optional
     }
@@ -603,6 +614,7 @@
      * @param hideSilentStatusIcons whether or not status bar icons should be hidden for silent
      *                              notifications
      */
+    @UiThread
     public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
         // optional
     }
@@ -620,6 +632,7 @@
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
      */
+    @UiThread
     public void onNotificationChannelModified(String pkg, UserHandle user,
             NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType) {
         // optional
@@ -638,6 +651,7 @@
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
      */
+    @UiThread
     public void onNotificationChannelGroupModified(String pkg, UserHandle user,
             NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType) {
         // optional
@@ -650,6 +664,7 @@
      * @param interruptionFilter The current
      *     {@link #getCurrentInterruptionFilter() interruption filter}.
      */
+    @UiThread
     public void onInterruptionFilterChanged(int interruptionFilter) {
         // optional
     }
@@ -1197,6 +1212,11 @@
      * <p>
      * Listen for updates using {@link #onInterruptionFilterChanged(int)}.
      *
+     * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) cannot modify the global interruption filter.
+     * Calling this method will instead activate or deactivate an
+     * {@link android.app.AutomaticZenRule} associated to the app.
+     *
      * <p>The service should wait for the {@link #onListenerConnected()} event
      * before performing this operation.
      *
diff --git a/core/java/android/service/notification/ZenAdapters.java b/core/java/android/service/notification/ZenAdapters.java
new file mode 100644
index 0000000..b249815
--- /dev/null
+++ b/core/java/android/service/notification/ZenAdapters.java
@@ -0,0 +1,135 @@
+/*
+ * 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.service.notification;
+
+import android.annotation.NonNull;
+import android.app.Flags;
+import android.app.NotificationManager.Policy;
+
+/**
+ * Converters between different Zen representations.
+ * @hide
+ */
+public class ZenAdapters {
+
+    /** Maps {@link Policy} to {@link ZenPolicy}. */
+    @NonNull
+    public static ZenPolicy notificationPolicyToZenPolicy(@NonNull Policy policy) {
+        ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder()
+                .allowAlarms(policy.allowAlarms())
+                .allowCalls(
+                        policy.allowCalls()
+                                ? notificationPolicySendersToZenPolicyPeopleType(
+                                        policy.allowCallsFrom())
+                        : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowConversations(
+                        policy.allowConversations()
+                                ? notificationPolicyConversationSendersToZenPolicy(
+                                        policy.allowConversationsFrom())
+                                : ZenPolicy.CONVERSATION_SENDERS_NONE)
+                .allowEvents(policy.allowEvents())
+                .allowMedia(policy.allowMedia())
+                .allowMessages(
+                        policy.allowMessages()
+                                ? notificationPolicySendersToZenPolicyPeopleType(
+                                        policy.allowMessagesFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowReminders(policy.allowReminders())
+                .allowRepeatCallers(policy.allowRepeatCallers())
+                .allowSystem(policy.allowSystem());
+
+        if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
+            zenPolicyBuilder.showBadges(policy.showBadges())
+                    .showFullScreenIntent(policy.showFullScreenIntents())
+                    .showInAmbientDisplay(policy.showAmbient())
+                    .showInNotificationList(policy.showInNotificationList())
+                    .showLights(policy.showLights())
+                    .showPeeking(policy.showPeeking())
+                    .showStatusBarIcons(policy.showStatusBarIcons());
+        }
+
+        if (Flags.modesApi()) {
+            zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels());
+        }
+
+        return zenPolicyBuilder.build();
+    }
+
+    /** Maps {@link ZenPolicy.PeopleType} enum to {@link Policy.PrioritySenders}. */
+    @Policy.PrioritySenders
+    public static int zenPolicyPeopleTypeToNotificationPolicySenders(
+            @ZenPolicy.PeopleType int zpPeopleType, @Policy.PrioritySenders int defaultResult) {
+        switch (zpPeopleType) {
+            case ZenPolicy.PEOPLE_TYPE_ANYONE:
+                return Policy.PRIORITY_SENDERS_ANY;
+            case ZenPolicy.PEOPLE_TYPE_CONTACTS:
+                return Policy.PRIORITY_SENDERS_CONTACTS;
+            case ZenPolicy.PEOPLE_TYPE_STARRED:
+                return Policy.PRIORITY_SENDERS_STARRED;
+            default:
+                return defaultResult;
+        }
+    }
+
+    /** Maps {@link Policy.PrioritySenders} enum to {@link ZenPolicy.PeopleType}. */
+    @ZenPolicy.PeopleType
+    public static int notificationPolicySendersToZenPolicyPeopleType(
+            @Policy.PrioritySenders int npPrioritySenders) {
+        switch (npPrioritySenders) {
+            case Policy.PRIORITY_SENDERS_ANY:
+                return ZenPolicy.PEOPLE_TYPE_ANYONE;
+            case Policy.PRIORITY_SENDERS_CONTACTS:
+                return ZenPolicy.PEOPLE_TYPE_CONTACTS;
+            case Policy.PRIORITY_SENDERS_STARRED:
+            default:
+                return ZenPolicy.PEOPLE_TYPE_STARRED;
+        }
+    }
+
+    /** Maps {@link ZenPolicy.ConversationSenders} enum to {@link Policy.ConversationSenders}. */
+    @Policy.ConversationSenders
+    public static int zenPolicyConversationSendersToNotificationPolicy(
+            @ZenPolicy.ConversationSenders int zpConversationSenders,
+            @Policy.ConversationSenders int defaultResult) {
+        switch (zpConversationSenders) {
+            case ZenPolicy.CONVERSATION_SENDERS_ANYONE:
+                return Policy.CONVERSATION_SENDERS_ANYONE;
+            case ZenPolicy.CONVERSATION_SENDERS_IMPORTANT:
+                return Policy.CONVERSATION_SENDERS_IMPORTANT;
+            case ZenPolicy.CONVERSATION_SENDERS_NONE:
+                return Policy.CONVERSATION_SENDERS_NONE;
+            default:
+                return defaultResult;
+        }
+    }
+
+    /** Maps {@link Policy.ConversationSenders} enum to {@link ZenPolicy.ConversationSenders}. */
+    @ZenPolicy.ConversationSenders
+    private static int notificationPolicyConversationSendersToZenPolicy(
+            @Policy.ConversationSenders int npPriorityConversationSenders) {
+        switch (npPriorityConversationSenders) {
+            case Policy.CONVERSATION_SENDERS_ANYONE:
+                return ZenPolicy.CONVERSATION_SENDERS_ANYONE;
+            case Policy.CONVERSATION_SENDERS_IMPORTANT:
+                return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
+            case Policy.CONVERSATION_SENDERS_NONE:
+                return ZenPolicy.CONVERSATION_SENDERS_NONE;
+            default: // including Policy.CONVERSATION_SENDERS_UNSET
+                return ZenPolicy.CONVERSATION_SENDERS_UNSET;
+        }
+    }
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index d9ca935..1d6dd1e 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -24,6 +24,9 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
+import static android.service.notification.ZenAdapters.notificationPolicySendersToZenPolicyPeopleType;
+import static android.service.notification.ZenAdapters.zenPolicyConversationSendersToNotificationPolicy;
+import static android.service.notification.ZenAdapters.zenPolicyPeopleTypeToNotificationPolicySenders;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -1269,11 +1272,11 @@
     public ZenPolicy toZenPolicy() {
         ZenPolicy.Builder builder = new ZenPolicy.Builder()
                 .allowCalls(allowCalls
-                        ? ZenModeConfig.getZenPolicySenders(allowCallsFrom)
+                        ? notificationPolicySendersToZenPolicyPeopleType(allowCallsFrom)
                         : ZenPolicy.PEOPLE_TYPE_NONE)
                 .allowRepeatCallers(allowRepeatCallers)
                 .allowMessages(allowMessages
-                        ? ZenModeConfig.getZenPolicySenders(allowMessagesFrom)
+                        ? notificationPolicySendersToZenPolicyPeopleType(allowMessagesFrom)
                         : ZenPolicy.PEOPLE_TYPE_NONE)
                 .allowReminders(allowReminders)
                 .allowEvents(allowEvents)
@@ -1333,14 +1336,14 @@
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_MESSAGES,
                 isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_MESSAGES, defaultPolicy))) {
             priorityCategories |= Policy.PRIORITY_CATEGORY_MESSAGES;
-            messageSenders = getNotificationPolicySenders(zenPolicy.getPriorityMessageSenders(),
-                    messageSenders);
+            messageSenders = zenPolicyPeopleTypeToNotificationPolicySenders(
+                    zenPolicy.getPriorityMessageSenders(), messageSenders);
         }
 
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS,
                 isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_CONVERSATIONS, defaultPolicy))) {
             priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
-            conversationSenders = getConversationSendersWithDefault(
+            conversationSenders = zenPolicyConversationSendersToNotificationPolicy(
                     zenPolicy.getPriorityConversationSenders(), conversationSenders);
         } else {
             conversationSenders = CONVERSATION_SENDERS_NONE;
@@ -1349,8 +1352,8 @@
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS,
                 isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_CALLS, defaultPolicy))) {
             priorityCategories |= Policy.PRIORITY_CATEGORY_CALLS;
-            callSenders = getNotificationPolicySenders(zenPolicy.getPriorityCallSenders(),
-                    callSenders);
+            callSenders = zenPolicyPeopleTypeToNotificationPolicySenders(
+                    zenPolicy.getPriorityCallSenders(), callSenders);
         }
 
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS,
@@ -1449,47 +1452,6 @@
         return (policy.suppressedVisualEffects & visualEffect) == 0;
     }
 
-    private static int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
-            int defaultPolicySender) {
-        switch (senders) {
-            case ZenPolicy.PEOPLE_TYPE_ANYONE:
-                return Policy.PRIORITY_SENDERS_ANY;
-            case ZenPolicy.PEOPLE_TYPE_CONTACTS:
-                return Policy.PRIORITY_SENDERS_CONTACTS;
-            case ZenPolicy.PEOPLE_TYPE_STARRED:
-                return Policy.PRIORITY_SENDERS_STARRED;
-            default:
-                return defaultPolicySender;
-        }
-    }
-
-    private static int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
-            int defaultPolicySender) {
-        switch (senders) {
-            case ZenPolicy.CONVERSATION_SENDERS_ANYONE:
-            case ZenPolicy.CONVERSATION_SENDERS_IMPORTANT:
-            case ZenPolicy.CONVERSATION_SENDERS_NONE:
-                return senders;
-            default:
-                return defaultPolicySender;
-        }
-    }
-
-    /**
-     * Maps NotificationManager.Policy senders type to ZenPolicy.PeopleType
-     */
-    public static @ZenPolicy.PeopleType int getZenPolicySenders(int senders) {
-        switch (senders) {
-            case Policy.PRIORITY_SENDERS_ANY:
-                return ZenPolicy.PEOPLE_TYPE_ANYONE;
-            case Policy.PRIORITY_SENDERS_CONTACTS:
-                return ZenPolicy.PEOPLE_TYPE_CONTACTS;
-            case Policy.PRIORITY_SENDERS_STARRED:
-            default:
-                return ZenPolicy.PEOPLE_TYPE_STARRED;
-        }
-    }
-
     public Policy toNotificationPolicy() {
         int priorityCategories = 0;
         int priorityCallSenders = Policy.PRIORITY_SENDERS_CONTACTS;
@@ -1524,7 +1486,7 @@
         }
         priorityCallSenders = sourceToPrioritySenders(allowCallsFrom, priorityCallSenders);
         priorityMessageSenders = sourceToPrioritySenders(allowMessagesFrom, priorityMessageSenders);
-        priorityConversationSenders = getConversationSendersWithDefault(
+        priorityConversationSenders = zenPolicyConversationSendersToNotificationPolicy(
                 allowConversationsFrom, priorityConversationSenders);
 
         int state = areChannelsBypassingDnd ? Policy.STATE_CHANNELS_BYPASSING_DND : 0;
@@ -1559,15 +1521,6 @@
         }
     }
 
-    private static int prioritySendersToSource(int prioritySenders, int def) {
-        switch (prioritySenders) {
-            case Policy.PRIORITY_SENDERS_CONTACTS: return SOURCE_CONTACT;
-            case Policy.PRIORITY_SENDERS_STARRED: return SOURCE_STAR;
-            case Policy.PRIORITY_SENDERS_ANY: return SOURCE_ANYONE;
-            default: return def;
-        }
-    }
-
     private static int normalizePrioritySenders(int prioritySenders, int def) {
         if (!(prioritySenders == Policy.PRIORITY_SENDERS_CONTACTS
                 || prioritySenders == Policy.PRIORITY_SENDERS_STARRED
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
index e44c69c..6dbff71 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -36,11 +36,13 @@
  */
 oneway interface IOnDeviceIntelligenceService {
     void getVersion(in RemoteCallback remoteCallback);
-    void getFeature(in int featureId, in IFeatureCallback featureCallback);
-    void listFeatures(in IListFeaturesCallback listFeaturesCallback);
-    void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
+    void getFeature(int callerUid, int featureId, in IFeatureCallback featureCallback);
+    void listFeatures(int callerUid, in IListFeaturesCallback listFeaturesCallback);
+    void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
     void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future);
     void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
-    void requestFeatureDownload(in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
+    void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
     void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
+    void notifyInferenceServiceConnected();
+    void notifyInferenceServiceDisconnected();
 }
\ No newline at end of file
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceTrustedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
similarity index 74%
rename from core/java/android/service/ondeviceintelligence/IOnDeviceTrustedInferenceService.aidl
rename to core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
index e3fda04..73257ed 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceTrustedInferenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
@@ -18,7 +18,7 @@
 
 import android.app.ondeviceintelligence.IStreamingResponseCallback;
 import android.app.ondeviceintelligence.IResponseCallback;
-import android.app.ondeviceintelligence.ITokenCountCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.app.ondeviceintelligence.Content;
 import android.app.ondeviceintelligence.Feature;
@@ -29,18 +29,18 @@
 import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
 
 /**
- * Interface for a concrete implementation to provide on device trusted inference.
+ * Interface for a concrete implementation to provide on-device sandboxed inference.
  *
  * @hide
  */
-oneway interface IOnDeviceTrustedInferenceService {
+oneway interface IOnDeviceSandboxedInferenceService {
     void registerRemoteStorageService(in IRemoteStorageService storageService);
-    void requestTokenCount(in Feature feature, in Content request, in ICancellationSignal cancellationSignal,
-                            in ITokenCountCallback tokenCountCallback);
-    void processRequest(in Feature feature, in Content request, in int requestType,
+    void requestTokenInfo(int callerUid, in Feature feature, in Content request, in ICancellationSignal cancellationSignal,
+                            in ITokenInfoCallback tokenInfoCallback);
+    void processRequest(int callerUid, in Feature feature, in Content request, in int requestType,
                         in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
                         in IResponseCallback callback);
-    void processRequestStreaming(in Feature feature, in Content request, in int requestType,
+    void processRequestStreaming(int callerUid, in Feature feature, in Content request, in int requestType,
                                 in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
                                 in IStreamingResponseCallback callback);
     void updateProcessingState(in Bundle processingState,
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
index 46ba25d..fce3689 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -65,7 +65,7 @@
 
 /**
  * Abstract base class for performing setup for on-device inference and providing file access to
- * the isolated counter part {@link OnDeviceTrustedInferenceService}.
+ * the isolated counter part {@link OnDeviceSandboxedInferenceService}.
  *
  * <p> A service that provides configuration and model files relevant to performing inference on
  * device. The system's default OnDeviceIntelligenceService implementation is configured in
@@ -110,6 +110,8 @@
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            // TODO(326052028) : Move the remote method calls to an app handler from the binder
+            //  thread.
             return new IOnDeviceIntelligenceService.Stub() {
                 /** {@inheritDoc} */
                 @Override
@@ -123,38 +125,40 @@
                 }
 
                 @Override
-                public void listFeatures(IListFeaturesCallback listFeaturesCallback) {
+                public void listFeatures(int callerUid,
+                        IListFeaturesCallback listFeaturesCallback) {
                     Objects.requireNonNull(listFeaturesCallback);
-                    OnDeviceIntelligenceService.this.onListFeatures(
+                    OnDeviceIntelligenceService.this.onListFeatures(callerUid,
                             wrapListFeaturesCallback(listFeaturesCallback));
                 }
 
                 @Override
-                public void getFeature(int id, IFeatureCallback featureCallback) {
+                public void getFeature(int callerUid, int id, IFeatureCallback featureCallback) {
                     Objects.requireNonNull(featureCallback);
-                    OnDeviceIntelligenceService.this.onGetFeature(id,
-                            wrapFeatureCallback(featureCallback));
+                    OnDeviceIntelligenceService.this.onGetFeature(callerUid,
+                            id, wrapFeatureCallback(featureCallback));
                 }
 
 
                 @Override
-                public void getFeatureDetails(Feature feature,
+                public void getFeatureDetails(int callerUid, Feature feature,
                         IFeatureDetailsCallback featureDetailsCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(featureDetailsCallback);
 
-                    OnDeviceIntelligenceService.this.onGetFeatureDetails(feature,
-                            wrapFeatureDetailsCallback(featureDetailsCallback));
+                    OnDeviceIntelligenceService.this.onGetFeatureDetails(callerUid,
+                            feature, wrapFeatureDetailsCallback(featureDetailsCallback));
                 }
 
                 @Override
-                public void requestFeatureDownload(Feature feature,
+                public void requestFeatureDownload(int callerUid, Feature feature,
                         ICancellationSignal cancellationSignal,
                         IDownloadCallback downloadCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(downloadCallback);
 
-                    OnDeviceIntelligenceService.this.onDownloadFeature(feature,
+                    OnDeviceIntelligenceService.this.onDownloadFeature(callerUid,
+                            feature,
                             CancellationSignal.fromTransport(cancellationSignal),
                             wrapDownloadCallback(downloadCallback));
                 }
@@ -188,12 +192,38 @@
                         IRemoteProcessingService remoteProcessingService) {
                     mRemoteProcessingService = remoteProcessingService;
                 }
+
+                @Override
+                public void notifyInferenceServiceConnected() {
+                    OnDeviceIntelligenceService.this.onInferenceServiceConnected();
+                }
+
+                @Override
+                public void notifyInferenceServiceDisconnected() {
+                    OnDeviceIntelligenceService.this.onInferenceServiceDisconnected();
+                }
             };
         }
         Slog.w(TAG, "Incorrect service interface, returning null.");
         return null;
     }
 
+
+    /**
+     * Invoked when a new instance of the remote inference service is created.
+     * This method should be used as a signal to perform any initialization operations, for e.g. by
+     * invoking the {@link #updateProcessingState} method to initialize the remote processing
+     * service.
+     */
+    public abstract void onInferenceServiceConnected();
+
+
+    /**
+     * Invoked when an instance of the remote inference service is disconnected.
+     */
+    public abstract void onInferenceServiceDisconnected();
+
+
     /**
      * Invoked by the {@link OnDeviceIntelligenceService} inorder to send updates to the inference
      * service if there is a state change to be performed.
@@ -391,6 +421,7 @@
      * Request download for feature that is requested and listen to download progress updates. If
      * the download completes successfully, success callback should be populated.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            the feature for which files need to be downlaoded.
      *                           process.
      * @param cancellationSignal signal to attach a listener to, and receive cancellation signals
@@ -398,7 +429,7 @@
      * @param downloadCallback   callback to populate download updates for clients to listen on..
      */
     public abstract void onDownloadFeature(
-            @NonNull Feature feature,
+            int callerUid, @NonNull Feature feature,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull DownloadCallback downloadCallback);
 
@@ -407,20 +438,22 @@
      * implementation use the {@link Feature#getFeatureParams()} as a hint to communicate what
      * details the client is looking for.
      *
-     * @param feature               the feature for which status needs to be known.
-     * @param featureStatusCallback callback to populate the resulting feature status.
+     * @param callerUid              UID of the caller that initiated this call chain.
+     * @param feature                the feature for which status needs to be known.
+     * @param featureDetailsCallback callback to populate the resulting feature status.
      */
-    public abstract void onGetFeatureDetails(@NonNull Feature feature,
+    public abstract void onGetFeatureDetails(int callerUid, @NonNull Feature feature,
             @NonNull OutcomeReceiver<FeatureDetails,
-                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureStatusCallback);
+                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureDetailsCallback);
 
 
     /**
      * Get feature using the provided identifier to the remote implementation.
      *
+     * @param callerUid       UID of the caller that initiated this call chain.
      * @param featureCallback callback to populate the features list.
      */
-    public abstract void onGetFeature(int featureId,
+    public abstract void onGetFeature(int callerUid, int featureId,
             @NonNull OutcomeReceiver<Feature,
                     OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureCallback);
 
@@ -428,9 +461,10 @@
      * List all features which are available in the remote implementation. The implementation might
      * choose to provide only a certain list of features based on the caller.
      *
+     * @param callerUid            UID of the caller that initiated this call chain.
      * @param listFeaturesCallback callback to populate the features list.
      */
-    public abstract void onListFeatures(@NonNull OutcomeReceiver<List<Feature>,
+    public abstract void onListFeatures(int callerUid, @NonNull OutcomeReceiver<List<Feature>,
             OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> listFeaturesCallback);
 
     /**
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceTrustedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
similarity index 73%
rename from core/java/android/service/ondeviceintelligence/OnDeviceTrustedInferenceService.java
rename to core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index 8600197..7f7f9c2 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceTrustedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -16,6 +16,7 @@
 
 package android.service.ondeviceintelligence;
 
+import static android.app.ondeviceintelligence.OnDeviceIntelligenceManager.AUGMENT_REQUEST_CONTENT_BUNDLE_KEY;
 import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
 
 import android.annotation.CallbackExecutor;
@@ -23,6 +24,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.app.Service;
 import android.app.ondeviceintelligence.Content;
@@ -30,14 +32,18 @@
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.app.ondeviceintelligence.IResponseCallback;
 import android.app.ondeviceintelligence.IStreamingResponseCallback;
-import android.app.ondeviceintelligence.ITokenCountCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
 import android.app.ondeviceintelligence.OnDeviceIntelligenceManager;
 import android.app.ondeviceintelligence.ProcessingSignal;
-import android.app.ondeviceintelligence.StreamingResponseReceiver;
+import android.app.ondeviceintelligence.ProcessingOutcomeReceiver;
+import android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver;
+import android.app.ondeviceintelligence.TokenInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
@@ -75,8 +81,8 @@
  *
  * <pre>
  * {@literal
- * <service android:name=".SampleTrustedInferenceService"
- *          android:permission="android.permission.BIND_ONDEVICE_TRUSTED_INFERENCE_SERVICE"
+ * <service android:name=".SampleSandboxedInferenceService"
+ *          android:permission="android.permission.BIND_ONDEVICE_SANDBOXED_INFERENCE_SERVICE"
  *          android:isolatedProcess="true">
  * </service>}
  * </pre>
@@ -85,18 +91,18 @@
  */
 @SystemApi
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
-public abstract class OnDeviceTrustedInferenceService extends Service {
-    private static final String TAG = OnDeviceTrustedInferenceService.class.getSimpleName();
+public abstract class OnDeviceSandboxedInferenceService extends Service {
+    private static final String TAG = OnDeviceSandboxedInferenceService.class.getSimpleName();
 
     /**
      * The {@link Intent} that must be declared as handled by the service. To be supported, the
      * service must also require the
-     * {@link android.Manifest.permission#BIND_ON_DEVICE_TRUSTED_INFERENCE_SERVICE}
+     * {@link android.Manifest.permission#BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE}
      * permission so that other applications can not abuse it.
      */
     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
     public static final String SERVICE_INTERFACE =
-            "android.service.ondeviceintelligence.OnDeviceTrustedInferenceService";
+            "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService";
 
     private IRemoteStorageService mRemoteStorageService;
 
@@ -107,7 +113,7 @@
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction())) {
-            return new IOnDeviceTrustedInferenceService.Stub() {
+            return new IOnDeviceSandboxedInferenceService.Stub() {
                 @Override
                 public void registerRemoteStorageService(IRemoteStorageService storageService) {
                     Objects.requireNonNull(storageService);
@@ -115,50 +121,48 @@
                 }
 
                 @Override
-                public void requestTokenCount(Feature feature, Content request,
+                public void requestTokenInfo(int callerUid, Feature feature, Content request,
                         ICancellationSignal cancellationSignal,
-                        ITokenCountCallback tokenCountCallback) {
+                        ITokenInfoCallback tokenInfoCallback) {
                     Objects.requireNonNull(feature);
-                    Objects.requireNonNull(tokenCountCallback);
-                    OnDeviceTrustedInferenceService.this.onCountTokens(feature,
+                    Objects.requireNonNull(tokenInfoCallback);
+                    OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid,
+                            feature,
                             request,
                             CancellationSignal.fromTransport(cancellationSignal),
-                            wrapTokenCountCallback(tokenCountCallback));
+                            wrapTokenInfoCallback(tokenInfoCallback));
                 }
 
                 @Override
-                public void processRequestStreaming(Feature feature, Content request,
+                public void processRequestStreaming(int callerUid, Feature feature, Content request,
                         int requestType, ICancellationSignal cancellationSignal,
                         IProcessingSignal processingSignal,
                         IStreamingResponseCallback callback) {
                     Objects.requireNonNull(feature);
-                    Objects.requireNonNull(request);
                     Objects.requireNonNull(callback);
 
-                    OnDeviceTrustedInferenceService.this.onProcessRequestStreaming(feature,
+                    OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid,
+                            feature,
                             request,
                             requestType,
                             CancellationSignal.fromTransport(cancellationSignal),
                             ProcessingSignal.fromTransport(processingSignal),
-                            wrapStreamingResponseCallback(callback)
-                    );
+                            wrapStreamingResponseCallback(callback));
                 }
 
                 @Override
-                public void processRequest(Feature feature, Content request,
+                public void processRequest(int callerUid, Feature feature, Content request,
                         int requestType, ICancellationSignal cancellationSignal,
                         IProcessingSignal processingSignal,
                         IResponseCallback callback) {
                     Objects.requireNonNull(feature);
-                    Objects.requireNonNull(request);
                     Objects.requireNonNull(callback);
 
-
-                    OnDeviceTrustedInferenceService.this.onProcessRequest(feature, request,
-                            requestType, CancellationSignal.fromTransport(cancellationSignal),
+                    OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature,
+                            request, requestType,
+                            CancellationSignal.fromTransport(cancellationSignal),
                             ProcessingSignal.fromTransport(processingSignal),
-                            wrapResponseCallback(callback)
-                    );
+                            wrapResponseCallback(callback));
                 }
 
                 @Override
@@ -167,7 +171,7 @@
                     Objects.requireNonNull(processingState);
                     Objects.requireNonNull(callback);
 
-                    OnDeviceTrustedInferenceService.this.onUpdateProcessingState(processingState,
+                    OnDeviceSandboxedInferenceService.this.onUpdateProcessingState(processingState,
                             wrapOutcomeReceiver(callback)
                     );
                 }
@@ -178,35 +182,37 @@
     }
 
     /**
-     * Invoked when caller  wants to obtain a count of number of tokens present in the passed in
-     * Request associated with the provided feature.
+     * Invoked when caller  wants to obtain token info related to the payload in the passed
+     * content, associated with the provided feature.
      * The expectation from the implementation is that when processing is complete, it
-     * should provide the token count in the {@link OutcomeReceiver#onResult}.
+     * should provide the token info in the {@link OutcomeReceiver#onResult}.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            feature which is associated with the request.
      * @param request            request that requires processing.
      * @param cancellationSignal Cancellation Signal to receive cancellation events from client and
      *                           configure a listener to.
-     * @param callback           callback to populate failure and full response for the provided
+     * @param callback           callback to populate failure or the token info for the provided
      *                           request.
      */
     @NonNull
-    public abstract void onCountTokens(
-            @NonNull Feature feature,
+    public abstract void onTokenInfoRequest(
+            int callerUid, @NonNull Feature feature,
             @NonNull Content request,
             @Nullable CancellationSignal cancellationSignal,
-            @NonNull OutcomeReceiver<Long,
+            @NonNull OutcomeReceiver<TokenInfo,
                     OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback);
 
     /**
      * Invoked when caller provides a request for a particular feature to be processed in a
      * streaming manner. The expectation from the implementation is that when processing the
      * request,
-     * it periodically populates the {@link StreamingResponseReceiver#onNewContent} to continuously
+     * it periodically populates the {@link StreamedProcessingOutcomeReceiver#onNewContent} to continuously
      * provide partial Content results for the caller to utilize. Optionally the implementation can
-     * provide the complete response in the {@link StreamingResponseReceiver#onResult} upon
+     * provide the complete response in the {@link StreamedProcessingOutcomeReceiver#onResult} upon
      * processing completion.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            feature which is associated with the request.
      * @param request            request that requires processing.
      * @param requestType        identifier representing the type of request.
@@ -218,13 +224,12 @@
      */
     @NonNull
     public abstract void onProcessRequestStreaming(
-            @NonNull Feature feature,
-            @NonNull Content request,
+            int callerUid, @NonNull Feature feature,
+            @Nullable Content request,
             @OnDeviceIntelligenceManager.RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
-            @NonNull StreamingResponseReceiver<Content, Content,
-                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback);
+            @NonNull StreamedProcessingOutcomeReceiver callback);
 
     /**
      * Invoked when caller provides a request for a particular feature to be processed in one shot
@@ -233,6 +238,7 @@
      * should
      * provide the complete response in the {@link OutcomeReceiver#onResult}.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            feature which is associated with the request.
      * @param request            request that requires processing.
      * @param requestType        identifier representing the type of request.
@@ -244,13 +250,12 @@
      */
     @NonNull
     public abstract void onProcessRequest(
-            @NonNull Feature feature,
-            @NonNull Content request,
+            int callerUid, @NonNull Feature feature,
+            @Nullable Content request,
             @OnDeviceIntelligenceManager.RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
-            @NonNull OutcomeReceiver<Content,
-                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback);
+            @NonNull ProcessingOutcomeReceiver callback);
 
 
     /**
@@ -335,6 +340,26 @@
         }
     }
 
+
+    /**
+     * Returns the {@link Executor} to use for incoming IPC from request sender into your service
+     * implementation. For e.g. see
+     * {@link ProcessingOutcomeReceiver#onDataAugmentRequest(Content,
+     * Consumer)} where we use the executor to populate the consumer.
+     * <p>
+     * Override this method in your {@link OnDeviceSandboxedInferenceService} implementation to
+     * provide the executor you want to use for incoming IPC.
+     *
+     * @return the {@link Executor} to use for incoming IPC from {@link OnDeviceIntelligenceManager}
+     * to {@link OnDeviceSandboxedInferenceService}.
+     */
+    @SuppressLint("OnNameExpected")
+    @NonNull
+    public Executor getCallbackExecutor() {
+        return new HandlerExecutor(Handler.createAsync(getMainLooper()));
+    }
+
+
     private RemoteCallback wrapResultReceiverAsReadOnly(
             @NonNull Consumer<Map<String, FileInputStream>> resultConsumer,
             @NonNull Executor executor) {
@@ -355,10 +380,9 @@
         });
     }
 
-    private OutcomeReceiver<Content,
-            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapResponseCallback(
+    private ProcessingOutcomeReceiver wrapResponseCallback(
             IResponseCallback callback) {
-        return new OutcomeReceiver<>() {
+        return new ProcessingOutcomeReceiver() {
             @Override
             public void onResult(@androidx.annotation.NonNull Content response) {
                 try {
@@ -378,13 +402,23 @@
                     Slog.e(TAG, "Error sending result: " + e);
                 }
             }
+
+            @Override
+            public void onDataAugmentRequest(@NonNull Content content,
+                    @NonNull Consumer<Content> contentCallback) {
+                try {
+                    callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback));
+
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending augment request: " + e);
+                }
+            }
         };
     }
 
-    private StreamingResponseReceiver<Content, Content,
-            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapStreamingResponseCallback(
+    private StreamedProcessingOutcomeReceiver wrapStreamingResponseCallback(
             IStreamingResponseCallback callback) {
-        return new StreamingResponseReceiver<>() {
+        return new StreamedProcessingOutcomeReceiver() {
             @Override
             public void onNewContent(@androidx.annotation.NonNull Content content) {
                 try {
@@ -413,17 +447,43 @@
                     Slog.e(TAG, "Error sending result: " + e);
                 }
             }
+
+            @Override
+            public void onDataAugmentRequest(@NonNull Content content,
+                    @NonNull Consumer<Content> contentCallback) {
+                try {
+                    callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback));
+
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending augment request: " + e);
+                }
+            }
         };
     }
 
-    private OutcomeReceiver<Long,
-            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapTokenCountCallback(
-            ITokenCountCallback tokenCountCallback) {
+    private RemoteCallback wrapRemoteCallback(
+            @androidx.annotation.NonNull Consumer<Content> contentCallback) {
+        return new RemoteCallback(
+                result -> {
+                    if (result != null) {
+                        getCallbackExecutor().execute(() -> contentCallback.accept(
+                                result.getParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY,
+                                        Content.class)));
+                    } else {
+                        getCallbackExecutor().execute(
+                                () -> contentCallback.accept(null));
+                    }
+                });
+    }
+
+    private OutcomeReceiver<TokenInfo,
+            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapTokenInfoCallback(
+            ITokenInfoCallback tokenInfoCallback) {
         return new OutcomeReceiver<>() {
             @Override
-            public void onResult(Long tokenCount) {
+            public void onResult(TokenInfo tokenInfo) {
                 try {
-                    tokenCountCallback.onSuccess(tokenCount);
+                    tokenInfoCallback.onSuccess(tokenInfo);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error sending result: " + e);
                 }
@@ -433,7 +493,7 @@
             public void onError(
                     OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException exception) {
                 try {
-                    tokenCountCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                    tokenInfoCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
                             exception.getErrorParams());
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error sending failure: " + e);
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 68e8c72..0ae3e59 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1733,6 +1733,8 @@
     private static native long nativeCopy(long destNativePtr, long sourceNativePtr,
             boolean keepHistory);
     @CriticalNative
+    private static native long nativeSplit(long destNativePtr, long sourceNativePtr, int idBits);
+    @CriticalNative
     private static native int nativeGetId(long nativePtr);
     @CriticalNative
     private static native int nativeGetDeviceId(long nativePtr);
@@ -1773,9 +1775,9 @@
     @CriticalNative
     private static native void nativeOffsetLocation(long nativePtr, float deltaX, float deltaY);
     @CriticalNative
-    private static native float nativeGetXOffset(long nativePtr);
+    private static native float nativeGetRawXOffset(long nativePtr);
     @CriticalNative
-    private static native float nativeGetYOffset(long nativePtr);
+    private static native float nativeGetRawYOffset(long nativePtr);
     @CriticalNative
     private static native float nativeGetXPrecision(long nativePtr);
     @CriticalNative
@@ -3743,7 +3745,7 @@
                     nativeGetAction(mNativePtr), nativeGetFlags(mNativePtr),
                     nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr),
                     nativeGetButtonState(mNativePtr), nativeGetClassification(mNativePtr),
-                    nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr),
+                    nativeGetRawXOffset(mNativePtr), nativeGetRawYOffset(mNativePtr),
                     nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr),
                     nativeGetDownTimeNanos(mNativePtr),
                     nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT),
@@ -3767,86 +3769,23 @@
     }
 
     /**
-     * Splits a motion event such that it includes only a subset of pointer ids.
+     * Splits a motion event such that it includes only a subset of pointer IDs.
+     * @param idBits the bitset indicating all of the pointer IDs from this motion event that should
+     *               be in the new split event. idBits must be a non-empty subset of the pointer IDs
+     *               contained in this event.
      * @hide
      */
+    // TODO(b/327503168): Pass downTime as a parameter to split.
     @UnsupportedAppUsage
+    @NonNull
     public final MotionEvent split(int idBits) {
-        MotionEvent ev = obtain();
-        synchronized (gSharedTempLock) {
-            final int oldPointerCount = nativeGetPointerCount(mNativePtr);
-            ensureSharedTempPointerCapacity(oldPointerCount);
-            final PointerProperties[] pp = gSharedTempPointerProperties;
-            final PointerCoords[] pc = gSharedTempPointerCoords;
-            final int[] map = gSharedTempPointerIndexMap;
-
-            final int oldAction = nativeGetAction(mNativePtr);
-            final int oldActionMasked = oldAction & ACTION_MASK;
-            final int oldActionPointerIndex = (oldAction & ACTION_POINTER_INDEX_MASK)
-                    >> ACTION_POINTER_INDEX_SHIFT;
-            int newActionPointerIndex = -1;
-            int newPointerCount = 0;
-            for (int i = 0; i < oldPointerCount; i++) {
-                nativeGetPointerProperties(mNativePtr, i, pp[newPointerCount]);
-                final int idBit = 1 << pp[newPointerCount].id;
-                if ((idBit & idBits) != 0) {
-                    if (i == oldActionPointerIndex) {
-                        newActionPointerIndex = newPointerCount;
-                    }
-                    map[newPointerCount] = i;
-                    newPointerCount += 1;
-                }
-            }
-
-            if (newPointerCount == 0) {
-                throw new IllegalArgumentException("idBits did not match any ids in the event");
-            }
-
-            final int newAction;
-            if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) {
-                if (newActionPointerIndex < 0) {
-                    // An unrelated pointer changed.
-                    newAction = ACTION_MOVE;
-                } else if (newPointerCount == 1) {
-                    // The first/last pointer went down/up.
-                    newAction = oldActionMasked == ACTION_POINTER_DOWN
-                            ? ACTION_DOWN
-                            : (getFlags() & FLAG_CANCELED) == 0 ? ACTION_UP : ACTION_CANCEL;
-                } else {
-                    // A secondary pointer went down/up.
-                    newAction = oldActionMasked
-                            | (newActionPointerIndex << ACTION_POINTER_INDEX_SHIFT);
-                }
-            } else {
-                // Simple up/down/cancel/move or other motion action.
-                newAction = oldAction;
-            }
-
-            final int historySize = nativeGetHistorySize(mNativePtr);
-            for (int h = 0; h <= historySize; h++) {
-                final int historyPos = h == historySize ? HISTORY_CURRENT : h;
-
-                for (int i = 0; i < newPointerCount; i++) {
-                    nativeGetPointerCoords(mNativePtr, map[i], historyPos, pc[i]);
-                }
-
-                final long eventTimeNanos = nativeGetEventTimeNanos(mNativePtr, historyPos);
-                if (h == 0) {
-                    ev.initialize(nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr),
-                            nativeGetDisplayId(mNativePtr),
-                            newAction, nativeGetFlags(mNativePtr),
-                            nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr),
-                            nativeGetButtonState(mNativePtr), nativeGetClassification(mNativePtr),
-                            nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr),
-                            nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr),
-                            nativeGetDownTimeNanos(mNativePtr), eventTimeNanos,
-                            newPointerCount, pp, pc);
-                } else {
-                    nativeAddBatch(ev.mNativePtr, eventTimeNanos, pc, 0);
-                }
-            }
-            return ev;
+        if (idBits == 0) {
+            throw new IllegalArgumentException(
+                    "idBits must contain at least one pointer from this motion event");
         }
+        MotionEvent event = obtain();
+        event.mNativePtr = nativeSplit(event.mNativePtr, this.mNativePtr, idBits);
+        return event;
     }
 
     /**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 828004b..a6f380d 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -25,6 +25,7 @@
 import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
@@ -5629,7 +5630,7 @@
     @Nullable
     private ViewTranslationCallback mViewTranslationCallback;
 
-    private float mFrameContentVelocity = 0;
+    private float mFrameContentVelocity = -1;
 
     @Nullable
 
@@ -5660,6 +5661,9 @@
     protected long mMinusTwoFrameIntervalMillis = 0;
     private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
 
+    private float mLastFrameX = Float.NaN;
+    private float mLastFrameY = Float.NaN;
+
     @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = Float.NaN;
     @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
@@ -24597,7 +24601,10 @@
     public void draw(@NonNull Canvas canvas) {
         final int privateFlags = mPrivateFlags;
         mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
-        mFrameContentVelocity = 0;
+
+        mFrameContentVelocity = -1;
+        mLastFrameX = mLeft + mRenderNode.getTranslationX();
+        mLastFrameY = mTop + mRenderNode.getTranslationY();
 
         /*
          * Draw traversal performs several drawing steps which must be executed
@@ -33673,6 +33680,17 @@
             if (sToolkitMetricsForFrameRateDecisionFlagValue) {
                 viewRootImpl.recordViewPercentage(sizePercentage);
             }
+            if (viewVelocityApi()) {
+                float velocity = mFrameContentVelocity;
+                if (velocity < 0f) {
+                    velocity = calculateVelocity();
+                }
+                if (velocity > 0f) {
+                    float frameRate = convertVelocityToFrameRate(velocity);
+                    viewRootImpl.votePreferredFrameRate(frameRate, FRAME_RATE_COMPATIBILITY_GTE);
+                    return;
+                }
+            }
             if (!Float.isNaN(mPreferredFrameRate)) {
                 if (mPreferredFrameRate < 0) {
                     if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
@@ -33695,6 +33713,23 @@
         }
     }
 
+    private float convertVelocityToFrameRate(float velocityPps) {
+        float density = getResources().getDisplayMetrics().density;
+        float velocityDps = velocityPps / density;
+        // Choose a frame rate in increments of 10fps
+        return Math.min(140f, 60f + (10f * (float) Math.floor(velocityDps / 300f)));
+    }
+
+    private float calculateVelocity() {
+        // This current calculation is very simple. If something on the screen moved, then
+        // it votes for the highest velocity. If it doesn't move, then return 0.
+        float x = mLeft + mRenderNode.getTranslationX();
+        float y = mTop + mRenderNode.getTranslationY();
+
+        return (!Float.isNaN(mLastFrameX) && (x != mLastFrameX || y != mLastFrameY))
+                ? 100_000f : 0f;
+    }
+
     /**
      * Set the current velocity of the View, we only track positive value.
      * We will use the velocity information to adjust the frame rate when applicable.
@@ -33725,7 +33760,7 @@
     @FlaggedApi(FLAG_VIEW_VELOCITY_API)
     public float getFrameContentVelocity() {
         if (viewVelocityApi()) {
-            return mFrameContentVelocity;
+            return (mFrameContentVelocity < 0f) ? 0f : mFrameContentVelocity;
         }
         return 0;
     }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index a7cb169..42f6405 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -31,6 +31,7 @@
 import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
 import static android.view.View.PFLAG_DRAW_ANIMATION;
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
@@ -3447,7 +3448,10 @@
             // other windows to resize/move based on the raw frame of the window, waiting until we
             // can finish laying out this window and get back to the window manager with the
             // ultimately computed insets.
-            insetsPending = computesInternalInsets;
+            insetsPending = computesInternalInsets
+                    // If this window provides insets via params, its insets source frame can be
+                    // updated directly without waiting for WindowSession#setInsets.
+                    && mWindowAttributes.providedInsets == null;
 
             if (mSurfaceHolder != null) {
                 mSurfaceHolder.mSurfaceLock.lock();
@@ -7568,7 +7572,8 @@
             }
 
             // For the variable refresh rate project
-            if (handled && shouldTouchBoost(action, mWindowAttributes.type)) {
+            if (handled && shouldTouchBoost(action & MotionEvent.ACTION_MASK,
+                    mWindowAttributes.type)) {
                 // set the frame rate to the maximum value.
                 mIsTouchBoosting = true;
                 setPreferredFrameRateCategory(mPreferredFrameRateCategory);
@@ -12395,6 +12400,17 @@
                     mFrameRateCompatibility).applyAsyncUnsafe();
                 mLastPreferredFrameRate = preferredFrameRate;
             }
+            if (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE && mIsTouchBoosting) {
+                // We've received a velocity, so we'll let the velocity control the
+                // frame rate unless we receive additional motion events.
+                mIsTouchBoosting = false;
+                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                    Trace.instant(
+                            Trace.TRACE_TAG_VIEW,
+                            "ViewRootImpl#setFrameRate velocity used, no touch boost on next frame"
+                    );
+                }
+            }
         } catch (Exception e) {
             Log.e(mTag, "Unable to set frame rate", e);
         } finally {
@@ -12420,9 +12436,8 @@
     }
 
     private boolean shouldTouchBoost(int motionEventAction, int windowType) {
-        boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN
-                || motionEventAction == MotionEvent.ACTION_MOVE
-                || motionEventAction == MotionEvent.ACTION_UP;
+        // boost for almost all input
+        boolean desiredAction = motionEventAction != MotionEvent.ACTION_OUTSIDE;
         boolean undesiredType = windowType == TYPE_INPUT_METHOD
                 && sToolkitFrameRateTypingReadOnlyFlagValue;
         // use toolkitSetFrameRate flag to gate the change
@@ -12528,6 +12543,14 @@
     }
 
     /**
+     * Returns whether touch boost is currently enabled.
+     */
+    @VisibleForTesting
+    public boolean getIsTouchBoosting() {
+        return mIsTouchBoosting;
+    }
+
+    /**
      * Get the value of mFrameRateCompatibility
      */
     @VisibleForTesting
diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java
index 131fca7..1efd375 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -315,7 +315,8 @@
     /**
      * Add to this view's child count.  This increases the current child count by
      * <var>num</var> children beyond what was last set by {@link #setChildCount}
-     * or {@link #addChildCount}.  The index at which the new child starts in the child
+     * or {@link #addChildCount}.  The index at which the new
+     * child starts in the child
      * array is returned.
      *
      * @param num The number of new children to add.
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 64e5a5b..cf6b9e5 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -1160,12 +1160,10 @@
 
         // denylist only applies to not important views
         if (!view.isImportantForAutofill() && isActivityDeniedForAutofill()) {
-            Log.d(TAG, "view is not autofillable - activity denied for autofill");
             return false;
         }
 
         if (isActivityAllowedForAutofill()) {
-            Log.d(TAG, "view is autofillable - activity allowed for autofill");
             return true;
         }
 
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 3be76cc..985f542 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1153,7 +1153,6 @@
                     }
                     final boolean startInput;
                     synchronized (mH) {
-                        mImeDispatcher.clear();
                         if (getBindSequenceLocked() != sequence) {
                             return;
                         }
@@ -2909,8 +2908,6 @@
      * @param flags {@link #HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} or {@code 0}
      * @param executor The executor to run the callback on.
      * @param callback {@code true>} would be received if delegation was accepted.
-     * @return {@code true} if view belongs to allowed delegate package declared in {@link
-     *     #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted
      * @see #prepareStylusHandwritingDelegation(View, String)
      * @see #acceptStylusHandwritingDelegation(View)
      */
diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java
index 3fc0a30..8501474 100644
--- a/core/java/android/webkit/WebViewDelegate.java
+++ b/core/java/android/webkit/WebViewDelegate.java
@@ -175,8 +175,16 @@
 
     /**
      * Adds the WebView asset path to {@link android.content.res.AssetManager}.
+     * If {@link android.content.res.Flags#FLAG_REGISTER_RESOURCE_PATHS} is enabled, this function
+     * will be a no-op because the asset paths appending work will only be handled by
+     * {@link android.content.res.Resources#registerResourcePaths(String, ApplicationInfo)},
+     * otherwise it behaves the old way.
      */
     public void addWebViewAssetPath(Context context) {
+        if (android.content.res.Flags.registerResourcePaths()) {
+            return;
+        }
+
         final String[] newAssetPaths =
                 WebViewFactory.getLoadedPackageInfo().applicationInfo.getAllApkPaths();
         final ApplicationInfo appInfo = context.getApplicationInfo();
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index c748a57..8f1b72e 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -30,6 +30,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.Signature;
+import android.content.res.Resources;
 import android.os.Build;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -93,6 +94,9 @@
     // error for namespace lookup
     public static final int LIBLOAD_FAILED_TO_FIND_NAMESPACE = 10;
 
+    // generic error for future use
+    static final int LIBLOAD_FAILED_OTHER = 11;
+
     /**
      * Stores the timestamps at which various WebView startup events occurred in this process.
      */
@@ -544,8 +548,14 @@
             Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
             try {
                 sTimestamps.mAddAssetsStart = SystemClock.uptimeMillis();
-                for (String newAssetPath : webViewContext.getApplicationInfo().getAllApkPaths()) {
-                    initialApplication.getAssets().addAssetPathAsSharedLibrary(newAssetPath);
+                if (android.content.res.Flags.registerResourcePaths()) {
+                    Resources.registerResourcePaths(webViewContext.getPackageName(),
+                            webViewContext.getApplicationInfo());
+                } else {
+                    for (String newAssetPath : webViewContext.getApplicationInfo()
+                            .getAllApkPaths()) {
+                        initialApplication.getAssets().addAssetPathAsSharedLibrary(newAssetPath);
+                    }
                 }
                 sTimestamps.mAddAssetsEnd = sTimestamps.mGetClassLoaderStart =
                         SystemClock.uptimeMillis();
diff --git a/core/java/android/webkit/WebViewProviderResponse.java b/core/java/android/webkit/WebViewProviderResponse.java
index 84e34a3..926aaff 100644
--- a/core/java/android/webkit/WebViewProviderResponse.java
+++ b/core/java/android/webkit/WebViewProviderResponse.java
@@ -40,6 +40,7 @@
                 STATUS_SUCCESS,
                 STATUS_FAILED_WAITING_FOR_RELRO,
                 STATUS_FAILED_LISTING_WEBVIEW_PACKAGES,
+                STATUS_FAILED_OTHER,
             })
     @Retention(RetentionPolicy.SOURCE)
     private @interface WebViewProviderStatus {}
@@ -49,6 +50,7 @@
             WebViewFactory.LIBLOAD_FAILED_WAITING_FOR_RELRO;
     public static final int STATUS_FAILED_LISTING_WEBVIEW_PACKAGES =
             WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES;
+    public static final int STATUS_FAILED_OTHER = WebViewFactory.LIBLOAD_FAILED_OTHER;
 
     public WebViewProviderResponse(
             @Nullable PackageInfo packageInfo, @WebViewProviderStatus int status) {
diff --git a/core/java/android/webkit/WebViewUpdateManager.java b/core/java/android/webkit/WebViewUpdateManager.java
index 8ada598..07576a2 100644
--- a/core/java/android/webkit/WebViewUpdateManager.java
+++ b/core/java/android/webkit/WebViewUpdateManager.java
@@ -127,7 +127,7 @@
      *
      * This choice will be stored persistently.
      *
-     * @param newProvider the package name to use, or null to reset to default.
+     * @param newProvider the package name to use.
      * @return the package name which is now in use, which may not be the
      *         requested one if it was not usable.
      */
@@ -155,7 +155,7 @@
     /**
      * Get the WebView provider which will be used if no explicit choice has been made.
      *
-     * The default provider is not guaranteed to be currently valid/usable.
+     * The default provider is not guaranteed to be a valid/usable WebView implementation.
      *
      * @return the default WebView provider.
      */
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 55b2251..0b99df3 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -16,6 +16,8 @@
 
 package android.widget;
 
+import static android.view.flags.Flags.viewVelocityApi;
+
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -1488,6 +1490,11 @@
             if (!awakenScrollBars()) {
                 postInvalidateOnAnimation();
             }
+
+            // For variable refresh rate project to track the current velocity of this View
+            if (viewVelocityApi()) {
+                setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+            }
         }
     }
 
@@ -1810,6 +1817,11 @@
                 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
                         maxScroll, 0, 0, width / 2, 0);
 
+                // For variable refresh rate project to track the current velocity of this View
+                if (viewVelocityApi()) {
+                    setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+                }
+
                 final boolean movingRight = velocityX > 0;
 
                 View currentFocused = findFocus();
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index fe4ac4e..a2d8d80 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -4299,7 +4299,7 @@
         }
 
         if (mode == MODE_NORMAL) {
-            mApplication = ApplicationInfo.CREATOR.createFromParcel(parcel);
+            mApplication = parcel.readTypedObject(ApplicationInfo.CREATOR);
             mIdealSize = parcel.readInt() == 0 ? null : SizeF.CREATOR.createFromParcel(parcel);
             mLayoutId = parcel.readInt();
             mViewId = parcel.readInt();
@@ -6805,7 +6805,7 @@
                 mBitmapCache.writeBitmapsToParcel(dest, flags);
                 mCollectionCache.writeToParcel(dest, flags, intentsToIgnore);
             }
-            mApplication.writeToParcel(dest, flags);
+            dest.writeTypedObject(mApplication, flags);
             if (mIsRoot || mIdealSize == null) {
                 dest.writeInt(0);
             } else {
@@ -6893,7 +6893,8 @@
      * @hide
      */
     public boolean hasSameAppInfo(ApplicationInfo info) {
-        return mApplication.packageName.equals(info.packageName) && mApplication.uid == info.uid;
+        return mApplication == null || mApplication.packageName.equals(info.packageName)
+                && mApplication.uid == info.uid;
     }
 
     /**
@@ -7672,8 +7673,7 @@
             byte[] instruction;
             final List<byte[]> instructions = new ArrayList<>(size);
             for (int i = 0; i < size; i++) {
-                instruction = new byte[in.readInt()];
-                in.readByteArray(instruction);
+                instruction = in.readBlob();
                 instructions.add(instruction);
             }
             return new DrawInstructions(instructions);
@@ -7688,8 +7688,7 @@
             final List<byte[]> instructions = drawInstructions.mInstructions;
             dest.writeInt(instructions.size());
             for (byte[] instruction : instructions) {
-                dest.writeInt(instruction.length);
-                dest.writeByteArray(instruction);
+                dest.writeBlob(instruction);
             }
         }
 
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index a1ebde7..42c2d80 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -16,6 +16,8 @@
 
 package android.widget;
 
+import static android.view.flags.Flags.viewVelocityApi;
+
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -726,6 +728,12 @@
                  * isFinished() is correct.
                 */
                 mScroller.computeScrollOffset();
+
+                // For variable refresh rate project to track the current velocity of this View
+                if (viewVelocityApi()) {
+                    setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+                }
+
                 mIsBeingDragged = !mScroller.isFinished() || !mEdgeGlowBottom.isFinished()
                     || !mEdgeGlowTop.isFinished();
                 // Catch the edge effect if it is active.
@@ -1573,6 +1581,11 @@
                 // Keep on drawing until the animation has finished.
                 postInvalidateOnAnimation();
             }
+
+            // For variable refresh rate project to track the current velocity of this View
+            if (viewVelocityApi()) {
+                setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+            }
         } else {
             if (mFlingStrictSpan != null) {
                 mFlingStrictSpan.finish();
@@ -1884,6 +1897,10 @@
             mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
                     Math.max(0, bottom - height), 0, height/2);
 
+            // For variable refresh rate project to track the current velocity of this View
+            if (viewVelocityApi()) {
+                setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+            }
             if (mFlingStrictSpan == null) {
                 mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
             }
diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java
index 65b5979..c769518 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -172,6 +172,20 @@
         newIntent.prepareToLeaveUser(callingUserId);
         final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
                 mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
+
+        if (isPrivateProfile(callingUserId)) {
+            buildAndExecuteForPrivateProfile(intentReceived, className, newIntent, callingUserId,
+                    targetUserId);
+        } else {
+            buildAndExecute(targetResolveInfoFuture, intentReceived, className, newIntent,
+                    callingUserId,
+                    targetUserId, userMessage, managedProfile);
+        }
+    }
+
+    private void buildAndExecute(CompletableFuture<ResolveInfo> targetResolveInfoFuture,
+            Intent intentReceived, String className, Intent newIntent, int callingUserId,
+            int targetUserId, String userMessage, UserInfo managedProfile) {
         targetResolveInfoFuture
                 .thenApplyAsync(targetResolveInfo -> {
                     if (isResolverActivityResolveInfo(targetResolveInfo)) {
@@ -195,6 +209,23 @@
                 }, getApplicationContext().getMainExecutor());
     }
 
+    private void buildAndExecuteForPrivateProfile(
+            Intent intentReceived, String className, Intent newIntent, int callingUserId,
+            int targetUserId) {
+        final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
+                mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
+        targetResolveInfoFuture
+                .thenAcceptAsync(targetResolveInfo -> {
+                    if (isResolverActivityResolveInfo(targetResolveInfo)) {
+                        launchResolverActivityWithCorrectTab(intentReceived, className, newIntent,
+                                callingUserId, targetUserId);
+                    } else {
+                        maybeShowUserConsentMiniResolverPrivate(targetResolveInfo, newIntent,
+                                targetUserId);
+                    }
+                }, getApplicationContext().getMainExecutor());
+    }
+
     private void maybeShowUserConsentMiniResolver(
             ResolveInfo target, Intent launchIntent, UserInfo managedProfile) {
         if (target == null || isIntentForwarderResolveInfo(target) || !isDeviceProvisioned()) {
@@ -233,24 +264,70 @@
                 "Showing user consent for redirection into the managed profile for intent [%s] and "
                         + " calling package [%s]",
                 launchIntent, callingPackage));
+        PackageManager packageManagerForTargetUser =
+                createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0)
+                        .getPackageManager();
+        buildMiniResolver(target, launchIntent, targetUserId,
+                getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser)),
+                packageManagerForTargetUser);
+
+
+        View telephonyInfo = findViewById(R.id.miniresolver_info_section);
+
+        // Additional information section is work telephony specific. Therefore, it is only shown
+        // for telephony related intents, when all sim subscriptions are in the work profile.
+        if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent))
+                && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType()
+                == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) {
+            telephonyInfo.setVisibility(View.VISIBLE);
+            ((TextView) findViewById(R.id.miniresolver_info_section_text))
+                    .setText(getWorkTelephonyInfoSectionMessage(launchIntent));
+        } else {
+            telephonyInfo.setVisibility(View.GONE);
+        }
+    }
+
+    private void maybeShowUserConsentMiniResolverPrivate(
+            ResolveInfo target, Intent launchIntent, int targetUserId) {
+        if (target == null || isIntentForwarderResolveInfo(target)) {
+            finish();
+            return;
+        }
+
+        String callingPackage = getCallingPackage();
+
+        Log.i("IntentForwarderActivity", String.format(
+                "Showing user consent for redirection into the main profile for intent [%s] and "
+                        + " calling package [%s]",
+                launchIntent, callingPackage));
+        PackageManager packageManagerForTargetUser =
+                createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0)
+                        .getPackageManager();
+        buildMiniResolver(target, launchIntent, targetUserId,
+                getString(R.string.miniresolver_open_in_personal,
+                        target.loadLabel(packageManagerForTargetUser)),
+                packageManagerForTargetUser);
+
+        View telephonyInfo = findViewById(R.id.miniresolver_info_section);
+        telephonyInfo.setVisibility(View.GONE);
+    }
+
+    private void buildMiniResolver(ResolveInfo target, Intent launchIntent, int targetUserId,
+            String resolverTitle, PackageManager pmForTargetUser) {
         int layoutId = R.layout.miniresolver;
         setContentView(layoutId);
 
         findViewById(R.id.title_container).setElevation(0);
 
-        PackageManager packageManagerForTargetUser =
-                createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0)
-                        .getPackageManager();
-
         ImageView icon = findViewById(R.id.icon);
         icon.setImageDrawable(
-                getAppIcon(target, launchIntent, targetUserId, packageManagerForTargetUser));
+                getAppIcon(target, launchIntent, targetUserId, pmForTargetUser));
 
         View buttonContainer = findViewById(R.id.button_bar_container);
         buttonContainer.setPadding(0, 0, 0, buttonContainer.getPaddingBottom());
 
         ((TextView) findViewById(R.id.open_cross_profile)).setText(
-                getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser)));
+                resolverTitle);
 
         // The mini-resolver's negative button is reused in this flow to cancel the intent
         ((Button) findViewById(R.id.use_same_profile_browser)).setText(R.string.cancel);
@@ -269,21 +346,6 @@
                     targetUserId);
             finish();
         });
-
-
-        View telephonyInfo = findViewById(R.id.miniresolver_info_section);
-
-        // Additional information section is work telephony specific. Therefore, it is only shown
-        // for telephony related intents, when all sim subscriptions are in the work profile.
-        if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent))
-                && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType()
-                    == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) {
-            telephonyInfo.setVisibility(View.VISIBLE);
-            ((TextView) findViewById(R.id.miniresolver_info_section_text))
-                    .setText(getWorkTelephonyInfoSectionMessage(launchIntent));
-        } else {
-            telephonyInfo.setVisibility(View.GONE);
-        }
     }
 
     private Drawable getAppIcon(
@@ -548,6 +610,18 @@
     }
 
     /**
+     * Returns the private profile for this device or null if there is no private profile.
+     */
+    @Nullable
+    private UserInfo getPrivateProfile() {
+        List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
+        for (UserInfo userInfo : relatedUsers) {
+            if (userInfo.isPrivateProfile()) return userInfo;
+        }
+        return null;
+    }
+
+    /**
      * Returns the userId of the profile parent or UserHandle.USER_NULL if there is
      * no parent.
      */
@@ -577,6 +651,17 @@
         return mMetricsLogger;
     }
 
+    private boolean isPrivateProfile(int userId) {
+        UserInfo privateProfile = getPrivateProfile();
+        return privateSpaceFlagsEnabled() && privateProfile != null
+                && privateProfile.id == userId;
+    }
+
+    private boolean privateSpaceFlagsEnabled() {
+        return android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
+    }
+
     @VisibleForTesting
     protected Injector createInjector() {
         return new InjectorImpl();
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 7dcbbea..78f06b6 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -2655,7 +2655,8 @@
 
     private boolean privateSpaceEnabled() {
         return mIsIntentPicker && android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.allowResolverSheetForPrivateSpace();
+                && android.multiuser.Flags.allowResolverSheetForPrivateSpace()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures();
     }
 
     /**
diff --git a/core/java/com/android/internal/app/SetScreenLockDialogActivity.java b/core/java/com/android/internal/app/SetScreenLockDialogActivity.java
index 93fe37c..360fcaf 100644
--- a/core/java/com/android/internal/app/SetScreenLockDialogActivity.java
+++ b/core/java/com/android/internal/app/SetScreenLockDialogActivity.java
@@ -75,7 +75,8 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         if (!(android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.showSetScreenLockDialog())) {
+                && android.multiuser.Flags.showSetScreenLockDialog()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures())) {
             finish();
             return;
         }
diff --git a/core/java/com/android/internal/app/UnlaunchableAppActivity.java b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
index 4ef0a1b..97f8084 100644
--- a/core/java/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
@@ -77,6 +77,7 @@
         }
 
         if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()
                 && !userManager.isManagedProfile(mUserId)) {
             Log.e(TAG, "Unlaunchable activity for target package " + targetPackageName
                     + " called for a non-managed-profile " + mUserId);
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index 85bdbb9..e55cdef 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -80,11 +80,10 @@
             in Bundle extras, in IntentSender resultIntent);
     boolean isRequestPinAppWidgetSupported();
     oneway void noteAppWidgetTapped(in String callingPackage, in int appWidgetId);
-    void setWidgetPreview(in ComponentName providerComponent, in int widgetCategories,
+    boolean setWidgetPreview(in ComponentName providerComponent, in int widgetCategories,
             in RemoteViews preview);
     @nullable RemoteViews getWidgetPreview(in String callingPackage,
             in ComponentName providerComponent, in int profileId, in int widgetCategory);
     void removeWidgetPreview(in ComponentName providerComponent, in int widgetCategories);
-
 }
 
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index bd806bf..91678c7 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -560,6 +560,19 @@
      */
     public static final String CURSOR_HOVER_STATES_ENABLED = "cursor_hover_states_enabled";
 
+
+    /*
+     * (long) The reset interval for generated preview API calls.
+     */
+    public static final String GENERATED_PREVIEW_API_RESET_INTERVAL_MS =
+            "generated_preview_api_reset_interval_ms";
+
+    /*
+     * (int) The max number of generated preview API calls per reset interval.
+     */
+    public static final String GENERATED_PREVIEW_API_MAX_CALLS_PER_INTERVAL =
+            "generated_preview_api_max_calls_per_interval";
+
     private SystemUiDeviceConfigFlags() {
     }
 }
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index 30de546..2096ba4 100644
--- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -55,37 +55,38 @@
  * A service for the ProtoLog logging system.
  */
 public class LegacyProtoLogImpl implements IProtoLog {
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
-
     private static final int BUFFER_CAPACITY = 1024 * 1024;
     private static final int PER_CHUNK_SIZE = 1024;
     private static final String TAG = "ProtoLog";
     private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
     static final String PROTOLOG_VERSION = "2.0.0";
-    private static final int DEFAULT_PER_CHUNK_SIZE = 0;
 
     private final File mLogFile;
     private final String mLegacyViewerConfigFilename;
     private final TraceBuffer mBuffer;
     private final LegacyProtoLogViewerConfigReader mViewerConfig;
+    private final TreeMap<String, IProtoLogGroup> mLogGroups;
     private final int mPerChunkSize;
 
     private boolean mProtoLogEnabled;
     private boolean mProtoLogEnabledLockFree;
     private final Object mProtoLogEnabledLock = new Object();
 
-    public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename) {
+    public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename,
+            TreeMap<String, IProtoLogGroup> logGroups) {
         this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY,
-                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE);
+                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, logGroups);
     }
 
     public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
-            LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize) {
+            LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize,
+            TreeMap<String, IProtoLogGroup> logGroups) {
         mLogFile = file;
         mBuffer = new TraceBuffer(bufferCapacity);
         mLegacyViewerConfigFilename = viewerConfigFilename;
         mViewerConfig = viewerConfig;
         mPerChunkSize = perChunkSize;
+        this.mLogGroups = logGroups;
     }
 
     /**
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 53062d8..4ead82f 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -22,9 +22,9 @@
 import static perfetto.protos.PerfettoTrace.InternedString.STR;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.BOOLEAN_PARAMS;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.DOUBLE_PARAMS;
-import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STACKTRACE_IID;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.MESSAGE_ID;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.SINT64_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STACKTRACE_IID;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STR_PARAM_IIDS;
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.GROUPS;
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.ID;
@@ -70,6 +70,8 @@
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData;
@@ -78,7 +80,6 @@
  * A service for the ProtoLog logging system.
  */
 public class PerfettoProtoLogImpl implements IProtoLog {
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
     private static final String LOG_TAG = "ProtoLog";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
 
@@ -89,8 +90,12 @@
     );
     private final ProtoLogViewerConfigReader mViewerConfigReader;
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    private final TreeMap<String, IProtoLogGroup> mLogGroups;
 
-    public PerfettoProtoLogImpl(String viewerConfigFilePath) {
+    private final ExecutorService mBackgroundLoggingService = Executors.newCachedThreadPool();
+
+    public PerfettoProtoLogImpl(String viewerConfigFilePath,
+            TreeMap<String, IProtoLogGroup> logGroups) {
         this(() -> {
             try {
                 return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
@@ -98,23 +103,28 @@
                 Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
                 return null;
             }
-        });
+        }, logGroups);
     }
 
-    public PerfettoProtoLogImpl(ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+    public PerfettoProtoLogImpl(
+            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            TreeMap<String, IProtoLogGroup> logGroups
+    ) {
         this(viewerConfigInputStreamProvider,
-                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
+                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), logGroups);
     }
 
     @VisibleForTesting
     public PerfettoProtoLogImpl(
             ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            ProtoLogViewerConfigReader viewerConfigReader
+            ProtoLogViewerConfigReader viewerConfigReader,
+            TreeMap<String, IProtoLogGroup> logGroups
     ) {
         Producer.init(InitArguments.DEFAULTS);
         mDataSource.register(DataSourceParams.DEFAULTS);
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
         this.mViewerConfigReader = viewerConfigReader;
+        this.mLogGroups = logGroups;
     }
 
     /**
@@ -128,7 +138,8 @@
 
         long tsNanos = SystemClock.elapsedRealtimeNanos();
         try {
-            logToProto(level, group.name(), messageHash, paramsMask, args, tsNanos);
+            mBackgroundLoggingService.submit(() ->
+                    logToProto(level, group.name(), messageHash, paramsMask, args, tsNanos));
             if (group.isLogToLogcat()) {
                 logToLogcat(group.getTag(), level, messageHash, messageString, args);
             }
@@ -456,40 +467,6 @@
     }
 
     /**
-     * Responds to a shell command.
-     */
-    public int onShellCommand(ShellCommand shell) {
-        PrintWriter pw = shell.getOutPrintWriter();
-        String cmd = shell.getNextArg();
-        if (cmd == null) {
-            return unknownCommand(pw);
-        }
-        ArrayList<String> args = new ArrayList<>();
-        String arg;
-        while ((arg = shell.getNextArg()) != null) {
-            args.add(arg);
-        }
-        final ILogger logger = (msg) -> logAndPrintln(pw, msg);
-        String[] groups = args.toArray(new String[args.size()]);
-        switch (cmd) {
-            case "enable-text":
-                return this.startLoggingToLogcat(groups, logger);
-            case "disable-text":
-                return this.stopLoggingToLogcat(groups, logger);
-            default:
-                return unknownCommand(pw);
-        }
-    }
-
-    private int unknownCommand(PrintWriter pw) {
-        pw.println("Unknown command");
-        pw.println("Window manager logging options:");
-        pw.println("  enable-text [group...]: Enable logcat logging for given groups");
-        pw.println("  disable-text [group...]: Disable logcat logging for given groups");
-        return -1;
-    }
-
-    /**
      * Returns {@code true} iff logging to proto is enabled.
      */
     public boolean isProtoEnabled() {
@@ -548,6 +525,49 @@
         return 0;
     }
 
+    /**
+     * Responds to a shell command.
+     */
+    public int onShellCommand(ShellCommand shell) {
+        PrintWriter pw = shell.getOutPrintWriter();
+        String cmd = shell.getNextArg();
+        if (cmd == null) {
+            return unknownCommand(pw);
+        }
+        ArrayList<String> args = new ArrayList<>();
+        String arg;
+        while ((arg = shell.getNextArg()) != null) {
+            args.add(arg);
+        }
+        final ILogger logger = (msg) -> logAndPrintln(pw, msg);
+        String[] groups = args.toArray(new String[0]);
+        switch (cmd) {
+            case "start", "stop" -> {
+                pw.println("Command not supported. "
+                        + "Please start and stop ProtoLog tracing with Perfetto.");
+                return -1;
+            }
+            case "enable-text" -> {
+                mViewerConfigReader.loadViewerConfig(logger);
+                return setTextLogging(true, logger, groups);
+            }
+            case "disable-text" -> {
+                return setTextLogging(false, logger, groups);
+            }
+            default -> {
+                return unknownCommand(pw);
+            }
+        }
+    }
+
+    private int unknownCommand(PrintWriter pw) {
+        pw.println("Unknown command");
+        pw.println("Window manager logging options:");
+        pw.println("  enable-text [group...]: Enable logcat logging for given groups");
+        pw.println("  disable-text [group...]: Disable logcat logging for given groups");
+        return -1;
+    }
+
     static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
         Slog.i(LOG_TAG, msg);
         if (pw != null) {
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 8965385..487ae814 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH;
 import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH;
+import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LOG_GROUPS;
 import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH;
 
 import android.annotation.Nullable;
@@ -28,6 +29,8 @@
 import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.common.ProtoLogToolInjected;
 
+import java.util.TreeMap;
+
 /**
  * A service for the ProtoLog logging system.
  */
@@ -43,6 +46,9 @@
     @ProtoLogToolInjected(LEGACY_OUTPUT_FILE_PATH)
     private static String sLegacyOutputFilePath;
 
+    @ProtoLogToolInjected(LOG_GROUPS)
+    private static TreeMap<String, IProtoLogGroup> sLogGroups;
+
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
     public static void d(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
@@ -99,11 +105,10 @@
     public static synchronized IProtoLog getSingleInstance() {
         if (sServiceInstance == null) {
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                sServiceInstance =
-                        new PerfettoProtoLogImpl(sViewerConfigPath);
+                sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sLogGroups);
             } else {
-                sServiceInstance =
-                        new LegacyProtoLogImpl(sLegacyOutputFilePath, sLegacyViewerConfigPath);
+                sServiceInstance = new LegacyProtoLogImpl(
+                        sLegacyOutputFilePath, sLegacyViewerConfigPath, sLogGroups);
             }
         }
         return sServiceInstance;
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index 3c206ac..ae3d448 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -4,6 +4,7 @@
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE;
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
 
+import android.util.ArrayMap;
 import android.util.proto.ProtoInputStream;
 
 import com.android.internal.protolog.common.ILogger;
@@ -57,6 +58,7 @@
     }
 
     private void doLoadViewerConfig() throws IOException {
+        mLogMessageMap = new ArrayMap<>();
         final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
 
         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
diff --git a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
index ffd0d76..17c82d7 100644
--- a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
+++ b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
@@ -22,7 +22,9 @@
 
 @Target({ElementType.FIELD, ElementType.PARAMETER})
 public @interface ProtoLogToolInjected {
-    enum Value { VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH }
+    enum Value {
+        VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH, LOG_GROUPS
+    }
 
     Value value();
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
index b7cb392..7c9fda5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -92,7 +92,7 @@
         mIndex = 0;
         mStartingIndex = 0;
         mSize = 0;
-        if (expectedSize > mMaxSize) {
+        if (expectedSize >= mMaxSize) {
             resize(expectedSize);
         }
     }
diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp
index d5f17da..83b6afa 100644
--- a/core/jni/LayoutlibLoader.cpp
+++ b/core/jni/LayoutlibLoader.cpp
@@ -196,14 +196,6 @@
     return result;
 }
 
-static vector<string> parseCsv(JNIEnv* env, jstring csvJString) {
-    const char* charArray = env->GetStringUTFChars(csvJString, 0);
-    string csvString(charArray);
-    vector<string> result = parseCsv(csvString);
-    env->ReleaseStringUTFChars(csvJString, charArray);
-    return result;
-}
-
 void LayoutlibLogger(base::LogId, base::LogSeverity severity, const char* tag, const char* file,
                      unsigned int line, const char* message) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -395,20 +387,28 @@
     jmethodID getPropertyMethod = GetStaticMethodIDOrDie(env, system, "getProperty",
                                                          "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
 
-    // Get the names of classes that need to register their native methods
-    auto nativesClassesJString =
-            (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
-                                                 env->NewStringUTF("core_native_classes"),
-                                                 env->NewStringUTF(""));
-    vector<string> classesToRegister = parseCsv(env, nativesClassesJString);
+    // Java system properties that contain LayoutLib config. The initial values in the map
+    // are the default values if the property is not specified.
+    std::unordered_map<std::string, std::string> systemProperties =
+            {{"core_native_classes", ""},
+             {"register_properties_during_load", ""},
+             {"icu.data.path", ""},
+             {"use_bridge_for_logging", ""},
+             {"keyboard_paths", ""}};
 
-    jstring registerProperty =
-            (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
-                                                 env->NewStringUTF(
-                                                         "register_properties_during_load"),
-                                                 env->NewStringUTF(""));
-    const char* registerPropertyString = env->GetStringUTFChars(registerProperty, 0);
-    if (strcmp(registerPropertyString, "true") == 0) {
+    for (auto& [name, defaultValue] : systemProperties) {
+        jstring propertyString =
+                (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
+                                                     env->NewStringUTF(name.c_str()),
+                                                     env->NewStringUTF(defaultValue.c_str()));
+        const char* propertyChars = env->GetStringUTFChars(propertyString, 0);
+        systemProperties[name] = string(propertyChars);
+        env->ReleaseStringUTFChars(propertyString, propertyChars);
+    }
+    // Get the names of classes that need to register their native methods
+    vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]);
+
+    if (systemProperties["register_properties_during_load"] == "true") {
         // Set the system properties first as they could be used in the static initialization of
         // other classes
         if (register_android_os_SystemProperties(env) < 0) {
@@ -423,35 +423,20 @@
         env->CallStaticVoidMethod(bridge, setSystemPropertiesMethod);
         property_initialize_ro_cpu_abilist();
     }
-    env->ReleaseStringUTFChars(registerProperty, registerPropertyString);
 
     if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
         return JNI_ERR;
     }
 
-    // Set the location of ICU data
-    auto stringPath = (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
-                                                           env->NewStringUTF("icu.data.path"),
-                                                           env->NewStringUTF(""));
-    const char* path = env->GetStringUTFChars(stringPath, 0);
-
-    if (strcmp(path, "**n/a**") != 0) {
-        bool icuInitialized = init_icu(path);
+    if (!systemProperties["icu.data.path"].empty()) {
+        // Set the location of ICU data
+        bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str());
         if (!icuInitialized) {
-            fprintf(stderr, "Failed to initialize ICU\n");
             return JNI_ERR;
         }
-    } else {
-        fprintf(stderr, "Skip initializing ICU\n");
     }
-    env->ReleaseStringUTFChars(stringPath, path);
 
-    jstring useJniProperty =
-            (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
-                                                 env->NewStringUTF("use_bridge_for_logging"),
-                                                 env->NewStringUTF(""));
-    const char* useJniString = env->GetStringUTFChars(useJniProperty, 0);
-    if (strcmp(useJniString, "true") == 0) {
+    if (systemProperties["use_bridge_for_logging"] == "true") {
         layoutLog = FindClassOrDie(env, "com/android/ide/common/rendering/api/ILayoutLog");
         layoutLog = MakeGlobalRefOrDie(env, layoutLog);
         logMethodId = GetMethodIDOrDie(env, layoutLog, "logAndroidFramework",
@@ -468,23 +453,16 @@
         // initialize logging, so ANDROD_LOG_TAGS env variable is respected
         android::base::InitLogging(nullptr, android::base::StderrLogger);
     }
-    env->ReleaseStringUTFChars(useJniProperty, useJniString);
 
     // Use English locale for number format to ensure correct parsing of floats when using strtof
     setlocale(LC_NUMERIC, "en_US.UTF-8");
 
-    auto keyboardPathsJString =
-            (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
-                                                 env->NewStringUTF("keyboard_paths"),
-                                                 env->NewStringUTF(""));
-    const char* keyboardPathsString = env->GetStringUTFChars(keyboardPathsJString, 0);
-    if (strcmp(keyboardPathsString, "**n/a**") != 0) {
-        vector<string> keyboardPaths = parseCsv(env, keyboardPathsJString);
+    if (!systemProperties["keyboard_paths"].empty()) {
+        vector<string> keyboardPaths = parseCsv(systemProperties["keyboard_paths"]);
         init_keyboard(env, keyboardPaths);
     } else {
         fprintf(stderr, "Skip initializing keyboard\n");
     }
-    env->ReleaseStringUTFChars(keyboardPathsJString, keyboardPathsString);
 
     return JNI_VERSION_1_6;
 }
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index b39d5cf..1a86363 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -411,8 +411,8 @@
             jniThrowNullPointerException(env, "pointerCoords");
             return;
         }
-        pointerCoordsToNative(env, pointerCoordsObj,
-                event->getXOffset(), event->getYOffset(), &rawPointerCoords[i]);
+        pointerCoordsToNative(env, pointerCoordsObj, event->getRawXOffset(), event->getRawYOffset(),
+                              &rawPointerCoords[i]);
         env->DeleteLocalRef(pointerCoordsObj);
     }
 
@@ -622,6 +622,18 @@
     return reinterpret_cast<jlong>(destEvent);
 }
 
+static jlong android_view_MotionEvent_nativeSplit(jlong destNativePtr, jlong sourceNativePtr,
+                                                  jint idBits) {
+    MotionEvent* destEvent = reinterpret_cast<MotionEvent*>(destNativePtr);
+    if (!destEvent) {
+        destEvent = new MotionEvent();
+    }
+    MotionEvent* sourceEvent = reinterpret_cast<MotionEvent*>(sourceNativePtr);
+    destEvent->splitFrom(*sourceEvent, static_cast<std::bitset<MAX_POINTER_ID + 1>>(idBits),
+                         InputEvent::nextId());
+    return reinterpret_cast<jlong>(destEvent);
+}
+
 static jint android_view_MotionEvent_nativeGetId(jlong nativePtr) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
     return event->getId();
@@ -723,14 +735,14 @@
     return event->offsetLocation(deltaX, deltaY);
 }
 
-static jfloat android_view_MotionEvent_nativeGetXOffset(jlong nativePtr) {
+static jfloat android_view_MotionEvent_nativeGetRawXOffset(jlong nativePtr) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
-    return event->getXOffset();
+    return event->getRawXOffset();
 }
 
-static jfloat android_view_MotionEvent_nativeGetYOffset(jlong nativePtr) {
+static jfloat android_view_MotionEvent_nativeGetRawYOffset(jlong nativePtr) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
-    return event->getYOffset();
+    return event->getRawYOffset();
 }
 
 static jfloat android_view_MotionEvent_nativeGetXPrecision(jlong nativePtr) {
@@ -837,6 +849,7 @@
         // --------------- @CriticalNative ------------------
 
         {"nativeCopy", "(JJZ)J", (void*)android_view_MotionEvent_nativeCopy},
+        {"nativeSplit", "(JJI)J", (void*)android_view_MotionEvent_nativeSplit},
         {"nativeGetId", "(J)I", (void*)android_view_MotionEvent_nativeGetId},
         {"nativeGetDeviceId", "(J)I", (void*)android_view_MotionEvent_nativeGetDeviceId},
         {"nativeGetSource", "(J)I", (void*)android_view_MotionEvent_nativeGetSource},
@@ -858,8 +871,8 @@
         {"nativeGetClassification", "(J)I",
          (void*)android_view_MotionEvent_nativeGetClassification},
         {"nativeOffsetLocation", "(JFF)V", (void*)android_view_MotionEvent_nativeOffsetLocation},
-        {"nativeGetXOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetXOffset},
-        {"nativeGetYOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetYOffset},
+        {"nativeGetRawXOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetRawXOffset},
+        {"nativeGetRawYOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetRawYOffset},
         {"nativeGetXPrecision", "(J)F", (void*)android_view_MotionEvent_nativeGetXPrecision},
         {"nativeGetYPrecision", "(J)F", (void*)android_view_MotionEvent_nativeGetYPrecision},
         {"nativeGetXCursorPosition", "(J)F",
diff --git a/core/proto/android/hardware/sensorprivacy.proto b/core/proto/android/hardware/sensorprivacy.proto
index e368c6a..53aa710 100644
--- a/core/proto/android/hardware/sensorprivacy.proto
+++ b/core/proto/android/hardware/sensorprivacy.proto
@@ -91,9 +91,7 @@
     enum StateType {
         ENABLED = 1;
         DISABLED = 2;
-        AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS = 3;
-        AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS = 4;
-        AUTO_DRIVER_ASSISTANCE_APPS = 5;
+        ENABLED_EXCEPT_ALLOWLISTED_APPS = 3;
     }
 
     // DEPRECATED
diff --git a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
index 5a18d9e..b75d545 100644
--- a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
+++ b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
@@ -39,7 +39,7 @@
     optional string cur_token = 14;
     optional int32 cur_token_display_id = 15;
     optional bool system_ready = 16;
-    optional int32 last_switch_user_id = 17;
+    reserved 17; // deprecated last_switch_user_id
     optional bool have_connection = 18;
     optional bool bound_to_method = 19;
     optional bool is_interactive = 20;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 487b5be..1acdc75 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6772,13 +6772,6 @@
     <permission android:name="android.permission.USE_BIOMETRIC_INTERNAL"
         android:protectionLevel="signature" />
 
-    <!-- Allows privileged apps to access the background face authentication.
-        @SystemApi
-        @FlaggedApi("android.hardware.biometrics.face_background_authentication")
-        @hide -->
-    <permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION"
-        android:protectionLevel="signature|privileged" />
-
     <!-- Allows the system to control the BiometricDialog (SystemUI). Reserved for the system. @hide -->
     <permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG"
                 android:protectionLevel="signature" />
@@ -7955,12 +7948,12 @@
         android:protectionLevel="signature|privileged" />
 
 
-    <!-- @SystemApi Allows an app to bind the on-device trusted service.
+    <!-- @SystemApi Allows an app to bind the on-device sandboxed service.
              <p>Protection level: signature|privileged
              @hide
          @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence")
         -->
-    <permission android:name="android.permission.BIND_ON_DEVICE_TRUSTED_SERVICE"
+    <permission android:name="android.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE"
         android:protectionLevel="signature"/>
 
 
@@ -8621,6 +8614,10 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
+        <service android:name="com.android.system.virtualmachine.SecretkeeperJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
         <service android:name="com.android.server.PruneInstantAppsJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
@@ -8675,7 +8672,7 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
-        <service android:name="com.android.server.companion.InactiveAssociationsRemovalService"
+        <service android:name="com.android.server.companion.association.InactiveAssociationsRemovalService"
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
diff --git a/core/res/res/values-watch/themes_device_defaults.xml b/core/res/res/values-watch/themes_device_defaults.xml
index df8158d..6e804c0 100644
--- a/core/res/res/values-watch/themes_device_defaults.xml
+++ b/core/res/res/values-watch/themes_device_defaults.xml
@@ -146,7 +146,7 @@
         <item name="windowAnimationStyle">@style/Animation.InputMethod</item>
         <item name="imeFullscreenBackground">?colorBackground</item>
         <item name="imeExtractEnterAnimation">@anim/input_method_extract_enter</item>
-        <item name="windowSwipeToDismiss">false</item>
+        <item name="windowSwipeToDismiss">true</item>
     </style>
 
     <!-- DeviceDefault theme for dialog windows and activities. In contrast to Material, the
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index c882938..d89f236 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -301,7 +301,9 @@
             granted to the system companion device manager service -->
         <flag name="companion" value="0x800000" />
         <!-- Additional flag from base permission type: this permission will be granted to the
-             retail demo app, as defined by the OEM. -->
+             retail demo app, as defined by the OEM.
+             This flag has been replaced by the retail demo role and is a no-op since Android V.
+          -->
         <flag name="retailDemo" value="0x1000000" />
         <!-- Additional flag from base permission type: this permission will be granted to the
              recents app. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 90f2731..9d902c9 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3551,10 +3551,6 @@
     <string name="config_keyguardComponent" translatable="false"
             >com.android.systemui/com.android.systemui.keyguard.KeyguardService</string>
 
-    <!-- Screen record dialog component -->
-    <string name="config_screenRecorderComponent" translatable="false"
-            >com.android.systemui/com.android.systemui.screenrecord.ScreenRecordDialog</string>
-
     <!-- The component name of a special dock app that merely launches a dream.
          We don't want to launch this app when docked because it causes an unnecessary
          activity transition.  We just want to start the dream. -->
@@ -4677,8 +4673,8 @@
     <!-- The component name for the default system on-device intelligence service, -->
     <string name="config_defaultOnDeviceIntelligenceService" translatable="false"></string>
 
-    <!-- The component name for the default system on-device trusted inference service. -->
-    <string name="config_defaultOnDeviceTrustedInferenceService" translatable="false"></string>
+    <!-- The component name for the default system on-device sandboxed inference service. -->
+    <string name="config_defaultOnDeviceSandboxedInferenceService" translatable="false"></string>
 
     <!-- Component name that accepts ACTION_SEND intents for requesting ambient context consent for
          wearable sensing. -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 59066eb..238772f 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6434,4 +6434,6 @@
     <string name="satellite_notification_open_message">Open Messages</string>
     <!-- Invoke Satellite setting activity of Settings -->
     <string name="satellite_notification_how_it_works">How it works</string>
+    <!-- Initial/System provided label shown for an app which gets unarchived. [CHAR LIMIT=64]. -->
+    <string name="unarchival_session_app_label">Pending...</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a025c8d..4b71654 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -375,7 +375,6 @@
   <java-symbol type="string" name="config_recentsComponentName" />
   <java-symbol type="string" name="config_systemUIServiceComponent" />
   <java-symbol type="string" name="config_controlsPackage" />
-  <java-symbol type="string" name="config_screenRecorderComponent" />
   <java-symbol type="string" name="config_somnambulatorComponent" />
   <java-symbol type="string" name="config_screenshotAppClipsServiceComponent" />
   <java-symbol type="string" name="config_screenshotServiceComponent" />
@@ -3917,7 +3916,7 @@
   <java-symbol type="string" name="config_ambientContextEventArrayExtraKey" />
   <java-symbol type="string" name="config_defaultWearableSensingService" />
   <java-symbol type="string" name="config_defaultOnDeviceIntelligenceService" />
-  <java-symbol type="string" name="config_defaultOnDeviceTrustedInferenceService" />
+  <java-symbol type="string" name="config_defaultOnDeviceSandboxedInferenceService" />
   <java-symbol type="string" name="config_retailDemoPackage" />
   <java-symbol type="string" name="config_retailDemoPackageSignature" />
 
@@ -5374,4 +5373,6 @@
   <java-symbol type="string" name="config_defaultContextualSearchKey" />
   <java-symbol type="string" name="config_defaultContextualSearchEnabled" />
   <java-symbol type="string" name="config_defaultContextualSearchLegacyEnabled" />
+
+  <java-symbol type="string" name="unarchival_session_app_label" />
 </resources>
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 61e6a36..7d740ef 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -88,6 +88,9 @@
     <!-- Colombia: 1-6 digits (not confirmed) -->
     <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289" />
 
+    <!-- Costa Rica  -->
+    <shortcode country="cr" pattern="\\d{1,6}" free="466453" />
+
     <!-- Cyprus: 4-6 digits (not confirmed), known premium codes listed, plus EU -->
     <shortcode country="cy" pattern="\\d{4,6}" premium="7510" free="116\\d{3}" />
 
@@ -143,6 +146,9 @@
     <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece -->
     <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" />
 
+    <!--  Guatemala  -->
+    <shortcode country="gt" pattern="\\d{1,6}" free="466453" />
+
     <!-- Croatia -->
     <shortcode country="hr" pattern="\\d{1,5}" free="13062" />
 
@@ -241,10 +247,10 @@
     <shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" />
 
     <!-- Pakistan -->
-    <shortcode country="pk" pattern="\\d{1,5}" free="2057" />
+    <shortcode country="pk" pattern="\\d{1,5}" free="2057|9092" />
 
     <!-- Palestine: 5 digits, known premium codes listed -->
-    <shortcode country="ps" pattern="\\d{1,5}" free="37477" />
+    <shortcode country="ps" pattern="\\d{1,5}" free="37477|6681" />
 
     <!-- Poland: 4-5 digits (not confirmed), known premium codes listed, plus EU -->
     <shortcode country="pl" pattern="\\d{4,5}" premium="74240|79(?:10|866)|92525" free="116\\d{3}|8012|80921" />
@@ -324,4 +330,7 @@
     <!-- South Africa -->
     <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056" />
 
+    <!-- Zimbabwe -->
+    <shortcode country="zw" pattern="\\d{1,5}" free="33679" />
+
 </shortcodes>
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 15bb66b..8d9fad9 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -90,7 +90,7 @@
     private static final long TEST_HD_FREQUENCY_VALUE = 95_300;
     private static final long TEST_HD_STATION_ID_EXT_VALUE = 0x100000001L
             | (TEST_HD_FREQUENCY_VALUE << 36);
-    private static final long TEST_HD_LOCATION_VALUE = 0x89CC8E06CCB9ECL;
+    private static final long TEST_HD_LOCATION_VALUE =  0x4E647007665CF6L;
     private static final long TEST_VENDOR_ID_VALUE = 9_901;
 
     private static final ProgramSelector.Identifier TEST_DAB_SID_EXT_ID =
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 37a499a..6cc5485 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -110,8 +110,6 @@
             Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
             Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
             Paths.get("/data/misc/wmtrace/wm_log.winscope"),
-            Paths.get("/data/misc/wmtrace/wm_transition_trace.winscope"),
-            Paths.get("/data/misc/wmtrace/shell_transition_trace.winscope"),
     };
 
     private Handler mHandler;
@@ -257,6 +255,38 @@
         assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
     }
 
+    @LargeTest
+    @Test
+    public void noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag() throws Exception {
+        startPreDumpedUiTraces();
+
+        mBrm.preDumpUiData();
+        waitTillDumpstateExitedOrTimeout();
+
+        // Simulate lost of pre-dumped data.
+        // For example it can happen in this scenario:
+        // 1. Pre-dump data
+        // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK)
+        // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK)
+        removeFilesIfNeeded(UI_TRACES_PREDUMPED);
+
+        // Start bugreport with "use predump" flag. Because the pre-dumped data is not available
+        // the flag will be ignored and data will be dumped as in normal flow.
+        BugreportCallbackImpl callback = new BugreportCallbackImpl();
+        mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
+                callback);
+        shareConsentDialog(ConsentReply.ALLOW);
+        waitTillDoneOrTimeout(callback);
+
+        stopPreDumpedUiTraces();
+
+        assertThat(callback.isDone()).isTrue();
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
+        assertFdsAreClosed(mBugreportFd);
+
+        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+    }
+
     @Test
     public void simultaneousBugreportsNotAllowed() throws Exception {
         // Start bugreport #1
@@ -506,9 +536,6 @@
         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
                 "cmd window tracing start"
         );
-        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
-                "service call SurfaceFlinger 1025 i32 1"
-        );
     }
 
     private static void stopPreDumpedUiTraces() {
@@ -611,6 +638,14 @@
         return files;
     }
 
+    private static void removeFilesIfNeeded(Path[] paths) throws Exception {
+        for (Path path : paths) {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "rm -f " + path.toString()
+            );
+        }
+    }
+
     private static ParcelFileDescriptor parcelFd(File file) throws Exception {
         return ParcelFileDescriptor.open(file,
                 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index e72beee..24031cad 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -101,6 +101,7 @@
         "flickerlib-trace_processor_shell",
         "mockito-target-extended-minus-junit4",
         "TestParameterInjector",
+        "android.content.res.flags-aconfig-java",
     ],
 
     libs: [
diff --git a/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml
index 866e1a9..5029212 100644
--- a/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml
+++ b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml
@@ -14,105 +14,150 @@
   ~ limitations under the License
   -->
 
-<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:id="@+id/horizontal_scroll_view">
+    android:orientation="vertical">
 
-    <LinearLayout
+    <HorizontalScrollView
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
+        android:layout_height="match_parent"
+        android:id="@+id/horizontal_scroll_view">
 
-        <View
-            android:background="#F00"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
 
-        <View
-            android:background="#880"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#F00"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#0F0"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#880"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#088"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#0F0"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#00F"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#088"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#808"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#00F"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#F00"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#808"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#880"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#F00"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#0F0"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#880"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#088"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#0F0"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#00F"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#088"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#808"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#00F"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#F00"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#808"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#880"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#F00"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#0F0"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#880"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#088"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#0F0"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#00F"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#088"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#808"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#00F"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-    </LinearLayout>
-</HorizontalScrollView>
+            <View
+                android:background="#808"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
+
+        </LinearLayout>
+    </HorizontalScrollView>
+
+    <view
+        class="android.widget.HorizontalScrollViewFunctionalTest$MyHorizontalScrollView"
+        android:id="@+id/my_horizontal_scroll_view"
+        android:layout_width="90dp"
+        android:layout_height="90dp"
+        android:background="#FFF"
+        android:defaultFocusHighlightEnabled="false">
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <View
+                android:background="#00F"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#0FF"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#0F0"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#FF0"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#F00"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#F0F"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+        </LinearLayout>
+    </view>
+</LinearLayout>
\ No newline at end of file
diff --git a/core/tests/coretests/res/layout/activity_scroll_view.xml b/core/tests/coretests/res/layout/activity_scroll_view.xml
index 61fabf8..db8cd02 100644
--- a/core/tests/coretests/res/layout/activity_scroll_view.xml
+++ b/core/tests/coretests/res/layout/activity_scroll_view.xml
@@ -14,105 +14,150 @@
   ~ limitations under the License
   -->
 
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:id="@+id/scroll_view">
+    android:orientation="vertical">
 
-    <LinearLayout
+    <ScrollView
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
+        android:layout_height="match_parent"
+        android:id="@+id/scroll_view">
 
-        <View
-            android:background="#F00"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
 
-        <View
-            android:background="#880"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#F00"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#0F0"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#880"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#088"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#0F0"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#00F"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#088"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#808"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#00F"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#F00"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#808"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#880"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#F00"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#0F0"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#880"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#088"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#0F0"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#00F"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#088"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#808"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#00F"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#F00"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#808"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#880"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#F00"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#0F0"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#880"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#088"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#0F0"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#00F"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#088"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-        <View
-            android:background="#808"
-            android:layout_width="100dp"
-            android:layout_height="100dp" />
+            <View
+                android:background="#00F"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
 
-    </LinearLayout>
-</ScrollView>
+            <View
+                android:background="#808"
+                android:layout_width="100dp"
+                android:layout_height="100dp" />
+
+        </LinearLayout>
+    </ScrollView>
+
+    <view
+        class="android.widget.ScrollViewFunctionalTest$MyScrollView"
+        android:id="@+id/my_scroll_view"
+        android:layout_width="90dp"
+        android:layout_height="90dp"
+        android:background="#FFF"
+        android:defaultFocusHighlightEnabled="false">
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <View
+                android:background="#00F"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#0FF"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#0F0"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#FF0"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#F00"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+            <View
+                android:background="#F0F"
+                android:layout_width="90dp"
+                android:layout_height="50dp"/>
+        </LinearLayout>
+    </view>
+</LinearLayout>
diff --git a/core/tests/coretests/res/layout/view_velocity_test.xml b/core/tests/coretests/res/layout/view_velocity_test.xml
new file mode 100644
index 0000000..98154a4
--- /dev/null
+++ b/core/tests/coretests/res/layout/view_velocity_test.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/frameLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <View
+        android:id="@+id/moving_view"
+        android:layout_width="50dp"
+        android:layout_height="50dp" />
+</FrameLayout>
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 2963acf..927c67c 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -988,7 +988,7 @@
     @NonNull
     private static ClientTransaction newStopTransaction(@NonNull Activity activity) {
         final StopActivityItem stopStateRequest = StopActivityItem.obtain(
-                activity.getActivityToken(), 0 /* configChanges */);
+                activity.getActivityToken());
 
         final ClientTransaction transaction = newTransaction(activity);
         transaction.addTransactionItem(stopStateRequest);
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index 4db5d1b..9907397 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -136,7 +136,7 @@
     @Test
     public void testDestroyActivityItem_preExecute() {
         final DestroyActivityItem item = DestroyActivityItem
-                .obtain(mActivityToken, false /* finished */, 123 /* configChanges */);
+                .obtain(mActivityToken, false /* finished */);
         item.preExecute(mHandler);
 
         assertEquals(1, mActivitiesToBeDestroyed.size());
@@ -146,7 +146,7 @@
     @Test
     public void testDestroyActivityItem_postExecute() {
         final DestroyActivityItem item = DestroyActivityItem
-                .obtain(mActivityToken, false /* finished */, 123 /* configChanges */);
+                .obtain(mActivityToken, false /* finished */);
         item.preExecute(mHandler);
         item.postExecute(mHandler, mPendingActions);
 
@@ -156,11 +156,11 @@
     @Test
     public void testDestroyActivityItem_execute() {
         final DestroyActivityItem item = DestroyActivityItem
-                .obtain(mActivityToken, false /* finished */, 123 /* configChanges */);
+                .obtain(mActivityToken, false /* finished */);
         item.execute(mHandler, mActivityClientRecord, mPendingActions);
 
         verify(mHandler).handleDestroyActivity(eq(mActivityClientRecord), eq(false) /* finishing */,
-                eq(123) /* configChanges */, eq(false) /* getNonConfigInstance */, any());
+                eq(false) /* getNonConfigInstance */, any());
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 31ea675..584fe16 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -98,7 +98,7 @@
 
     @Test
     public void testRecycleDestroyActivityItem() {
-        testRecycle(() -> DestroyActivityItem.obtain(mActivityToken, true, 117));
+        testRecycle(() -> DestroyActivityItem.obtain(mActivityToken, true));
     }
 
     @Test
@@ -169,7 +169,7 @@
 
     @Test
     public void testRecyclePauseActivityItemItem() {
-        testRecycle(() -> PauseActivityItem.obtain(mActivityToken, true, true, 5, true, true));
+        testRecycle(() -> PauseActivityItem.obtain(mActivityToken, true, true, true, true));
     }
 
     @Test
@@ -185,7 +185,7 @@
 
     @Test
     public void testRecycleStopItem() {
-        testRecycle(() -> StopActivityItem.obtain(mActivityToken, 4));
+        testRecycle(() -> StopActivityItem.obtain(mActivityToken));
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index adb6f2a..935bc75 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -306,7 +306,7 @@
         final IBinder token = mock(IBinder.class);
         final ClientTransaction destroyTransaction = ClientTransaction.obtain(null /* client */);
         destroyTransaction.addTransactionItem(
-                DestroyActivityItem.obtain(token, false /* finished */, 0 /* configChanges */));
+                DestroyActivityItem.obtain(token, false /* finished */));
         destroyTransaction.preExecute(mTransactionHandler);
         // The activity should be added to to-be-destroyed container.
         assertEquals(1, activitiesToBeDestroyed.size());
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index 75347bf..d451fe5 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -155,8 +155,7 @@
 
     @Test
     public void testDestroy() {
-        DestroyActivityItem item = DestroyActivityItem.obtain(mActivityToken, true /* finished */,
-                135 /* configChanges */);
+        DestroyActivityItem item = DestroyActivityItem.obtain(mActivityToken, true /* finished */);
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
@@ -244,8 +243,7 @@
     public void testPause() {
         // Write to parcel
         PauseActivityItem item = PauseActivityItem.obtain(mActivityToken, true /* finished */,
-                true /* userLeaving */, 135 /* configChanges */, true /* dontReport */,
-                true /* autoEnteringPip */);
+                true /* userLeaving */, true /* dontReport */, true /* autoEnteringPip */);
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
@@ -272,7 +270,7 @@
     @Test
     public void testStop() {
         // Write to parcel
-        StopActivityItem item = StopActivityItem.obtain(mActivityToken, 14 /* configChanges */);
+        StopActivityItem item = StopActivityItem.obtain(mActivityToken);
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
@@ -305,8 +303,7 @@
         ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
                 mActivityToken, config(), new ActivityWindowInfo());
 
-        StopActivityItem lifecycleRequest = StopActivityItem.obtain(mActivityToken,
-                78 /* configChanges */);
+        StopActivityItem lifecycleRequest = StopActivityItem.obtain(mActivityToken);
 
         ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         transaction.addTransactionItem(callback1);
@@ -351,8 +348,7 @@
         mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
 
         // Write to parcel
-        StopActivityItem lifecycleRequest = StopActivityItem.obtain(mActivityToken,
-                78 /* configChanges */);
+        StopActivityItem lifecycleRequest = StopActivityItem.obtain(mActivityToken);
 
         ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         transaction.addTransactionItem(lifecycleRequest);
diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
index 4a9cb71..0c1e879 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
@@ -18,34 +18,52 @@
 
 import android.annotation.NonNull;
 import android.app.ResourcesManager;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.LocaleList;
 import android.platform.test.annotations.Postsubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
 import android.view.Display;
 import android.view.DisplayAdjustments;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import junit.framework.TestCase;
 
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
 @Postsubmit
+@RunWith(AndroidJUnit4.class)
 public class ResourcesManagerTest extends TestCase {
     private static final int SECONDARY_DISPLAY_ID = 1;
     private static final String APP_ONE_RES_DIR = "app_one.apk";
     private static final String APP_ONE_RES_SPLIT_DIR = "app_one_split.apk";
     private static final String APP_TWO_RES_DIR = "app_two.apk";
     private static final String LIB_RES_DIR = "lib.apk";
+    private static final String TEST_LIB = "com.android.frameworks.coretests.bdr_helper_app1";
 
     private ResourcesManager mResourcesManager;
     private Map<Integer, DisplayMetrics> mDisplayMetricsMap;
+    private PackageManager mPackageManager;
 
-    @Override
-    protected void setUp() throws Exception {
+    @Before
+    public void setUp() throws Exception {
         super.setUp();
 
         mDisplayMetricsMap = new HashMap<>();
@@ -93,8 +111,14 @@
                 return mDisplayMetricsMap.get(displayId);
             }
         };
+
+        mPackageManager = InstrumentationRegistry.getContext().getPackageManager();
     }
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Test
     @SmallTest
     public void testMultipleCallsWithIdenticalParametersCacheReference() {
         Resources resources = mResourcesManager.getResources(
@@ -109,6 +133,7 @@
         assertSame(resources.getImpl(), newResources.getImpl());
     }
 
+    @Test
     @SmallTest
     public void testMultipleCallsWithDifferentParametersReturnDifferentReferences() {
         Resources resources = mResourcesManager.getResources(
@@ -125,6 +150,7 @@
         assertNotSame(resources, newResources);
     }
 
+    @Test
     @SmallTest
     public void testAddingASplitCreatesANewImpl() {
         Resources resources1 = mResourcesManager.getResources(
@@ -142,6 +168,7 @@
         assertNotSame(resources1.getImpl(), resources2.getImpl());
     }
 
+    @Test
     @SmallTest
     public void testUpdateConfigurationUpdatesAllAssetManagers() {
         Resources resources1 = mResourcesManager.getResources(
@@ -187,6 +214,7 @@
         assertEquals(expectedConfig, resources3.getConfiguration());
     }
 
+    @Test
     @SmallTest
     public void testTwoActivitiesWithIdenticalParametersShareImpl() {
         Binder activity1 = new Binder();
@@ -208,6 +236,7 @@
         assertSame(resources1.getImpl(), resources2.getImpl());
     }
 
+    @Test
     @SmallTest
     public void testThemesGetUpdatedWithNewImpl() {
         Binder activity1 = new Binder();
@@ -237,6 +266,7 @@
         assertTrue(value.data != 0);
     }
 
+    @Test
     @SmallTest
     public void testMultipleResourcesForOneActivityGetUpdatedWhenActivityBaseUpdates() {
         Binder activity1 = new Binder();
@@ -286,6 +316,7 @@
         assertEquals(expectedConfig2, resources2.getConfiguration());
     }
 
+    @Test
     @SmallTest
     public void testChangingActivityDisplayDoesntOverrideDisplayRequestedByResources() {
         Binder activity = new Binder();
@@ -322,4 +353,101 @@
         assertEquals(mDisplayMetricsMap.get(Display.DEFAULT_DISPLAY).widthPixels,
                 defaultDisplayResources.getDisplayMetrics().widthPixels);
     }
+
+    @Test
+    @SmallTest
+    @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    public void testExistingResourcesAfterResourcePathsRegistration()
+             throws PackageManager.NameNotFoundException {
+        // Inject ResourcesManager instance from this test to the ResourcesManager class so that all
+        // the static method can interact with this test smoothly.
+        ResourcesManager oriResourcesManager = ResourcesManager.getInstance();
+        ResourcesManager.setInstance(mResourcesManager);
+
+        // Create a Resources before register resources' paths for a package.
+        Resources resources = mResourcesManager.getResources(
+                null, APP_ONE_RES_DIR, null, null, null, null, null, null,
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
+        assertNotNull(resources);
+        ResourcesImpl oriResImpl = resources.getImpl();
+
+        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        Resources.registerResourcePaths(TEST_LIB, appInfo);
+
+        assertNotSame(oriResImpl, resources.getImpl());
+
+        String[] resourcePaths = appInfo.getAllApkPaths();
+        resourcePaths = removeDuplicates(resourcePaths);
+        ApkAssets[] loadedAssets = resources.getAssets().getApkAssets();
+        assertTrue(allResourcePathsLoaded(resourcePaths, loadedAssets));
+
+        // Package resources' paths should be cached in ResourcesManager.
+        assertEquals(Arrays.toString(resourcePaths), Arrays.toString(ResourcesManager.getInstance()
+                .getSharedLibAssetsMap().get(TEST_LIB).getAllAssetPaths()));
+
+        // Revert the ResourcesManager instance back.
+        ResourcesManager.setInstance(oriResourcesManager);
+    }
+
+    @Test
+    @SmallTest
+    @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    public void testNewResourcesAfterResourcePathsRegistration()
+            throws PackageManager.NameNotFoundException {
+        // Inject ResourcesManager instance from this test to the ResourcesManager class so that all
+        // the static method can interact with this test smoothly.
+        ResourcesManager oriResourcesManager = ResourcesManager.getInstance();
+        ResourcesManager.setInstance(mResourcesManager);
+
+        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        Resources.registerResourcePaths(TEST_LIB, appInfo);
+
+        // Create a Resources after register resources' paths for a package.
+        Resources resources = mResourcesManager.getResources(
+                null, APP_ONE_RES_DIR, null, null, null, null, null, null,
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
+        assertNotNull(resources);
+
+        String[] resourcePaths = appInfo.getAllApkPaths();
+        resourcePaths = removeDuplicates(resourcePaths);
+        ApkAssets[] loadedAssets = resources.getAssets().getApkAssets();
+        assertTrue(allResourcePathsLoaded(resourcePaths, loadedAssets));
+
+        // Package resources' paths should be cached in ResourcesManager.
+        assertEquals(Arrays.toString(resourcePaths), Arrays.toString(ResourcesManager.getInstance()
+                .getSharedLibAssetsMap().get(TEST_LIB).getAllAssetPaths()));
+
+        // Revert the ResourcesManager instance back.
+        ResourcesManager.setInstance(oriResourcesManager);
+    }
+
+    private static boolean allResourcePathsLoaded(String[] resourcePaths, ApkAssets[] loadedAsset) {
+        for (int i = 0; i < resourcePaths.length; i++) {
+            if (!resourcePaths[i].endsWith(".apk")) {
+                continue;
+            }
+            boolean found = false;
+            for (int j = 0; j < loadedAsset.length; j++) {
+                if (loadedAsset[j].getAssetPath().equals(resourcePaths[i])) {
+                    found = true;
+                }
+            }
+            if (!found) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static String[] removeDuplicates(String[] paths) {
+        var pathList = new ArrayList<String>();
+        var pathSet = new ArraySet<String>();
+        final int pathsLen = paths.length;
+        for (int i = 0; i < pathsLen; i++) {
+            if (pathSet.add(paths[i])) {
+                pathList.add(paths[i]);
+            }
+        }
+        return pathList.toArray(new String[0]);
+    }
 }
diff --git a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
index 34f5841..3a872b5 100644
--- a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
+++ b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
@@ -18,7 +18,6 @@
 
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS;
-import static android.hardware.biometrics.Flags.FLAG_FACE_BACKGROUND_AUTHENTICATION;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -36,15 +35,12 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.res.Resources;
-import android.hardware.biometrics.BiometricPrompt;
 import android.os.CancellationSignal;
 import android.os.Handler;
-import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsEnabled;
 
 import com.android.internal.R;
 
@@ -62,7 +58,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.Executor;
 
 @Presubmit
 @RunWith(MockitoJUnitRunner.class)
@@ -83,8 +78,6 @@
     @Mock
     private FaceManager.AuthenticationCallback mAuthCallback;
     @Mock
-    private BiometricPrompt.AuthenticationCallback mBioAuthCallback;
-    @Mock
     private FaceManager.EnrollmentCallback mEnrollmentCallback;
     @Mock
     private FaceManager.FaceDetectionCallback mFaceDetectionCallback;
@@ -98,16 +91,13 @@
     private TestLooper mLooper;
     private Handler mHandler;
     private FaceManager mFaceManager;
-    private Executor mExecutor;
 
     @Before
     public void setUp() throws Exception {
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
-        mExecutor = new HandlerExecutor(mHandler);
 
         when(mContext.getMainLooper()).thenReturn(mLooper.getLooper());
-        when(mContext.getMainExecutor()).thenReturn(mExecutor);
         when(mContext.getOpPackageName()).thenReturn(PACKAGE_NAME);
         when(mContext.getAttributionTag()).thenReturn(ATTRIBUTION_TAG);
         when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
@@ -169,19 +159,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_FACE_BACKGROUND_AUTHENTICATION)
-    public void authenticateInBackground_errorWhenUnavailable() throws Exception {
-        when(mService.authenticateInBackground(any(), anyLong(), any(), any()))
-                .thenThrow(new RemoteException());
-
-        mFaceManager.authenticateInBackground(mExecutor, null, new CancellationSignal(),
-                mBioAuthCallback);
-        mLooper.dispatchAll();
-
-        verify(mBioAuthCallback).onAuthenticationError(eq(FACE_ERROR_HW_UNAVAILABLE), any());
-    }
-
-    @Test
     public void enrollment_errorWhenFaceEnrollmentExists() throws RemoteException {
         when(mResources.getInteger(R.integer.config_faceMaxTemplatesPerUser)).thenReturn(1);
         when(mService.getEnrolledFaces(anyInt(), anyInt(), anyString()))
diff --git a/core/tests/coretests/src/android/view/ViewVelocityTest.java b/core/tests/coretests/src/android/view/ViewVelocityTest.java
new file mode 100644
index 0000000..d437f7b
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewVelocityTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ViewVelocityTest {
+
+    @Rule
+    public ActivityTestRule<ViewCaptureTestActivity> mActivityRule = new ActivityTestRule<>(
+            ViewCaptureTestActivity.class);
+
+    private Activity mActivity;
+    private View mMovingView;
+    private ViewRootImpl mViewRoot;
+
+    @Before
+    public void setUp() throws Throwable {
+        mActivity = mActivityRule.getActivity();
+        mActivityRule.runOnUiThread(() -> {
+            mActivity.setContentView(R.layout.view_velocity_test);
+            mMovingView = mActivity.findViewById(R.id.moving_view);
+        });
+        ViewParent parent = mActivity.getWindow().getDecorView().getParent();
+        while (parent instanceof View) {
+            parent = parent.getParent();
+        }
+        mViewRoot = (ViewRootImpl) parent;
+    }
+
+    @UiThreadTest
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void frameRateChangesWhenContentMoves() {
+        mMovingView.offsetLeftAndRight(100);
+        float frameRate = mViewRoot.getPreferredFrameRate();
+        assertTrue(frameRate > 0);
+    }
+
+    @UiThreadTest
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void firstFrameNoMovement() {
+        assertEquals(0f, mViewRoot.getPreferredFrameRate(), 0f);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void touchBoostDisable() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            long now = SystemClock.uptimeMillis();
+            MotionEvent down = MotionEvent.obtain(
+                    /* downTime */ now,
+                    /* eventTime */ now,
+                    /* action */ MotionEvent.ACTION_DOWN,
+                    /* x */ 0f,
+                    /* y */ 0f,
+                    /* metaState */ 0
+            );
+            mActivity.dispatchTouchEvent(down);
+            mMovingView.offsetLeftAndRight(10);
+        });
+        mActivityRule.runOnUiThread(() -> {
+            mMovingView.invalidate();
+        });
+
+        mActivityRule.runOnUiThread(() -> {
+            assertFalse(mViewRoot.getIsTouchBoosting());
+        });
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java
index df212eb..cd38bd6 100644
--- a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java
+++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java
@@ -16,11 +16,17 @@
 
 package android.widget;
 
+import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.AttributeSet;
 import android.util.PollingCheck;
 
 import androidx.test.filters.MediumTest;
@@ -43,14 +49,21 @@
 public class HorizontalScrollViewFunctionalTest {
     private HorizontalScrollViewActivity mActivity;
     private HorizontalScrollView mHorizontalScrollView;
+    private MyHorizontalScrollView mMyHorizontalScrollView;
     @Rule
     public ActivityTestRule<HorizontalScrollViewActivity> mActivityRule = new ActivityTestRule<>(
             HorizontalScrollViewActivity.class);
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mActivity = mActivityRule.getActivity();
         mHorizontalScrollView = mActivity.findViewById(R.id.horizontal_scroll_view);
+        mMyHorizontalScrollView =
+                (MyHorizontalScrollView) mActivity.findViewById(R.id.my_horizontal_scroll_view);
     }
 
     @Test
@@ -79,6 +92,22 @@
         assertEquals(maxScroll, mHorizontalScrollView.getScrollX());
     }
 
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void testSetVelocity() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mMyHorizontalScrollView.setFrameContentVelocity(0);
+        });
+        // set setFrameContentVelocity shouldn't do anything.
+        assertEquals(mMyHorizontalScrollView.isSetVelocityCalled, false);
+
+        mActivityRule.runOnUiThread(() -> {
+            mMyHorizontalScrollView.fling(100);
+        });
+        // set setFrameContentVelocity should be called when fling.
+        assertEquals(mMyHorizontalScrollView.isSetVelocityCalled, true);
+    }
+
     static class WatchedEdgeEffect extends EdgeEffect {
         public CountDownLatch onAbsorbLatch = new CountDownLatch(1);
 
@@ -92,5 +121,29 @@
             onAbsorbLatch.countDown();
         }
     }
+
+    public static class MyHorizontalScrollView extends ScrollView {
+
+        public boolean isSetVelocityCalled;
+
+        public MyHorizontalScrollView(Context context) {
+            super(context);
+        }
+
+        public MyHorizontalScrollView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        @Override
+        public void setFrameContentVelocity(float pixelsPerSecond) {
+            if (pixelsPerSecond != 0) {
+                isSetVelocityCalled = true;
+            }
+        }
+    }
 }
 
diff --git a/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java
index 109c808..a60b2a13 100644
--- a/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java
+++ b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java
@@ -16,11 +16,17 @@
 
 package android.widget;
 
+import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.AttributeSet;
 import android.util.PollingCheck;
 
 import androidx.test.filters.MediumTest;
@@ -43,14 +49,20 @@
 public class ScrollViewFunctionalTest {
     private ScrollViewActivity mActivity;
     private ScrollView mScrollView;
+    private MyScrollView mMyScrollView;
     @Rule
     public ActivityTestRule<ScrollViewActivity> mActivityRule = new ActivityTestRule<>(
             ScrollViewActivity.class);
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mActivity = mActivityRule.getActivity();
         mScrollView = mActivity.findViewById(R.id.scroll_view);
+        mMyScrollView = (MyScrollView) mActivity.findViewById(R.id.my_scroll_view);
     }
 
     @Test
@@ -79,6 +91,22 @@
         assertEquals(maxScroll, mScrollView.getScrollY());
     }
 
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void testSetVelocity() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            mMyScrollView.setFrameContentVelocity(0);
+        });
+        // set setFrameContentVelocity shouldn't do anything.
+        assertEquals(mMyScrollView.isSetVelocityCalled, false);
+
+        mActivityRule.runOnUiThread(() -> {
+            mMyScrollView.fling(100);
+        });
+        // set setFrameContentVelocity should be called when fling.
+        assertEquals(mMyScrollView.isSetVelocityCalled, true);
+    }
+
     static class WatchedEdgeEffect extends EdgeEffect {
         public CountDownLatch onAbsorbLatch = new CountDownLatch(1);
 
@@ -92,5 +120,29 @@
             onAbsorbLatch.countDown();
         }
     }
+
+    public static class MyScrollView extends ScrollView {
+
+        public boolean isSetVelocityCalled;
+
+        public MyScrollView(Context context) {
+            super(context);
+        }
+
+        public MyScrollView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public MyScrollView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        @Override
+        public void setFrameContentVelocity(float pixelsPerSecond) {
+            if (pixelsPerSecond != 0) {
+                isSetVelocityCalled = true;
+            }
+        }
+    }
 }
 
diff --git a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
index 58cfc66..43e6227 100644
--- a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
@@ -53,6 +53,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 
 import androidx.test.InstrumentationRegistry;
@@ -93,6 +94,9 @@
     private static final String TYPE_PLAIN_TEXT = "text/plain";
 
     private static UserInfo MANAGED_PROFILE_INFO = new UserInfo();
+    private static UserInfo PRIVATE_PROFILE_INFO = new UserInfo(12, "Private", null,
+            UserInfo.FLAG_PROFILE, UserManager.USER_TYPE_PROFILE_PRIVATE);
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     static {
         MANAGED_PROFILE_INFO.id = 10;
@@ -131,6 +135,7 @@
 
     @Before
     public void setup() {
+
         MockitoAnnotations.initMocks(this);
         mContext = InstrumentationRegistry.getTargetContext();
         sInjector = spy(new TestInjector());
@@ -632,6 +637,54 @@
                 logMakerCaptor.getValue().getSubtype());
     }
 
+    @Test
+    public void shouldForwardToParent_telephony_privateProfile() throws Exception {
+        mSetFlagsRule.enableFlags(
+                android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION);
+
+        sComponentName = FORWARD_TO_PARENT_COMPONENT_NAME;
+        when(mIPm.canForwardTo(
+                any(Intent.class), nullable(String.class), anyInt(), anyInt())).thenReturn(true);
+
+        List<UserInfo> profiles = new ArrayList<>();
+        profiles.add(CURRENT_USER_INFO);
+        profiles.add(PRIVATE_PROFILE_INFO);
+        when(mUserManager.getProfiles(anyInt())).thenReturn(profiles);
+        when(mUserManager.getProfileParent(anyInt())).thenReturn(CURRENT_USER_INFO);
+        Intent intent = new Intent(mContext, IntentForwarderWrapperActivity.class);
+        intent.setAction(Intent.ACTION_DIAL);
+        IntentForwarderWrapperActivity activity = mActivityRule.launchActivity(intent);
+        verify(mIPm).canForwardTo(any(), any(), anyInt(), anyInt());
+        assertEquals(activity.getStartActivityIntent().getAction(), intent.getAction());
+        assertEquals(activity.getUserIdActivityLaunchedIn(), CURRENT_USER_INFO.id);
+    }
+
+    @Test
+    public void shouldForwardToParent_mms_privateProfile() throws Exception {
+        mSetFlagsRule.enableFlags(
+                android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION);
+
+        sComponentName = FORWARD_TO_PARENT_COMPONENT_NAME;
+        when(mIPm.canForwardTo(
+                any(Intent.class), nullable(String.class), anyInt(), anyInt())).thenReturn(true);
+
+        List<UserInfo> profiles = new ArrayList<>();
+        profiles.add(CURRENT_USER_INFO);
+        profiles.add(PRIVATE_PROFILE_INFO);
+        when(mUserManager.getProfiles(anyInt())).thenReturn(profiles);
+        when(mUserManager.getProfileParent(anyInt())).thenReturn(CURRENT_USER_INFO);
+        Intent intent = new Intent(mContext, IntentForwarderWrapperActivity.class);
+        intent.setAction(Intent.ACTION_SEND);
+        intent.setType(TYPE_PLAIN_TEXT);
+        IntentForwarderWrapperActivity activity = mActivityRule.launchActivity(intent);
+        verify(mIPm).canForwardTo(any(), any(), anyInt(), anyInt());
+        assertEquals(activity.getStartActivityIntent().getAction(), intent.getAction());
+        assertEquals(activity.getStartActivityIntent().getType(), intent.getType());
+        assertEquals(activity.getUserIdActivityLaunchedIn(), CURRENT_USER_INFO.id);
+    }
+
     private void setupShouldSkipDisclosureTest() throws RemoteException {
         sComponentName = FORWARD_TO_PARENT_COMPONENT_NAME;
         sActivityName = "MyTestActivity";
@@ -688,6 +741,14 @@
         protected MetricsLogger getMetricsLogger() {
             return mMetricsLogger;
         }
+
+        Intent getStartActivityIntent() {
+            return mStartActivityIntent;
+        }
+
+        int getUserIdActivityLaunchedIn() {
+            return mUserIdActivityLaunchedIn;
+        }
     }
 
     public class TestInjector implements IntentForwarderActivity.Injector {
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index b209c7c..cb8754a 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -1162,7 +1162,8 @@
     @Test
     public void testTriggerFromPrivateProfile_withoutWorkProfile() throws RemoteException {
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         markPrivateProfileUserAvailable();
         Intent sendIntent = createSendImageIntent();
         List<ResolvedComponentInfo> privateResolvedComponentInfos =
@@ -1183,7 +1184,8 @@
     @Test
     public void testTriggerFromPrivateProfile_withWorkProfilePresent(){
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         ResolverActivity.ENABLE_TABBED_VIEW = false;
         markPrivateProfileUserAvailable();
         markWorkProfileUserAvailable();
@@ -1205,7 +1207,8 @@
     @Test
     public void testPrivateProfile_triggerFromPrimaryUser_withWorkProfilePresent(){
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         markPrivateProfileUserAvailable();
         markWorkProfileUserAvailable();
         Intent sendIntent = createSendImageIntent();
@@ -1228,7 +1231,8 @@
     @Test
     public void testPrivateProfile_triggerFromWorkProfile(){
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         markPrivateProfileUserAvailable();
         markWorkProfileUserAvailable();
         Intent sendIntent = createSendImageIntent();
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index 66be05f..ed641e0 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -309,17 +309,17 @@
 
         private void pauseActivity(ActivityClientRecord r) {
             mThread.handlePauseActivity(r, false /* finished */,
-                    false /* userLeaving */, 0 /* configChanges */, false /* autoEnteringPip */,
+                    false /* userLeaving */, false /* autoEnteringPip */,
                     null /* pendingActions */, "test");
         }
 
         private void stopActivity(ActivityClientRecord r) {
-            mThread.handleStopActivity(r, 0 /* configChanges */,
+            mThread.handleStopActivity(r,
                     new PendingTransactionActions(), false /* finalStateRequest */, "test");
         }
 
         private void destroyActivity(ActivityClientRecord r) {
-            mThread.handleDestroyActivity(r, true /* finishing */, 0 /* configChanges */,
+            mThread.handleDestroyActivity(r, true /* finishing */,
                     false /* getNonConfigInstance */, "test");
         }
 
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 051e73f..0f12438 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -455,7 +455,7 @@
         <permission name="android.permission.USE_BIOMETRIC" />
         <permission name="android.permission.TEST_BIOMETRIC" />
         <permission name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" />
-        <permission name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
+        <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" />
         <!-- Permissions required for CTS test - CtsContactsProviderTestCases -->
         <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" />
         <!-- Permissions required for CTS test - CtsHdmiCecHostTestCases -->
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index b6ce9b6..f9ac02a 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -789,10 +789,8 @@
      * @param m The 4x4 matrix to preconcatenate with the current matrix
      */
     @FlaggedApi(Flags.FLAG_MATRIX_44)
-    public void concat44(@Nullable Matrix44 m) {
-        if (m != null) {
-            nConcat(mNativeCanvasWrapper, m.mBackingArray);
-        }
+    public void concat(@Nullable Matrix44 m) {
+        if (m != null) nConcat(mNativeCanvasWrapper, m.mBackingArray);
     }
 
     /**
diff --git a/graphics/java/android/graphics/Matrix44.java b/graphics/java/android/graphics/Matrix44.java
index 7cc0eb7..a99e201 100644
--- a/graphics/java/android/graphics/Matrix44.java
+++ b/graphics/java/android/graphics/Matrix44.java
@@ -17,6 +17,7 @@
 package android.graphics;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 
 import com.android.graphics.hwui.flags.Flags;
@@ -98,11 +99,11 @@
     /**
      * Gets the value at the matrix's row and column.
      *
-     * @param row An integer from 0 to 4 indicating the row of the value to get
-     * @param col An integer from 0 to 4 indicating the column of the value to get
+     * @param row An integer from 0 to 3 indicating the row of the value to get
+     * @param col An integer from 0 to 3 indicating the column of the value to get
      */
     @FlaggedApi(Flags.FLAG_MATRIX_44)
-    public float get(int row, int col) {
+    public float get(@IntRange(from = 0, to = 3) int row, @IntRange(from = 0, to = 3) int col) {
         if (row >= 0 && row < 4 && col >= 0 && col < 4) {
             return mBackingArray[row * 4 + col];
         }
@@ -112,12 +113,13 @@
     /**
      * Sets the value at the matrix's row and column to the provided value.
      *
-     * @param row An integer from 0 to 4 indicating the row of the value to change
-     * @param col An integer from 0 to 4 indicating the column of the value to change
+     * @param row An integer from 0 to 3 indicating the row of the value to change
+     * @param col An integer from 0 to 3 indicating the column of the value to change
      * @param val The value the element at the specified index will be set to
      */
     @FlaggedApi(Flags.FLAG_MATRIX_44)
-    public void set(int row, int col, float val) {
+    public void set(@IntRange(from = 0, to = 3) int row, @IntRange(from = 0, to = 3) int col,
+            float val) {
         if (row >= 0 && row < 4 && col >= 0 && col < 4) {
             mBackingArray[row * 4 + col] = val;
         } else {
diff --git a/libs/WindowManager/Shell/res/drawable/circular_progress.xml b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
index 9482645..294b1f0 100644
--- a/libs/WindowManager/Shell/res/drawable/circular_progress.xml
+++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
@@ -25,7 +25,7 @@
             <shape
                 android:shape="ring"
                 android:thickness="3dp"
-                android:innerRadius="17dp"
+                android:innerRadius="14dp"
                 android:useLevel="true">
             </shape>
         </rotate>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml
index 02b7075..e5fe1b54 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml
@@ -15,12 +15,12 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="48dp"
-    android:height="48dp"
+    android:width="24dp"
+    android:height="24dp"
     android:tint="?attr/colorControlNormal"
     android:viewportHeight="960"
     android:viewportWidth="960">
     <path
-        android:fillColor="@android:color/white"
-        android:pathData="M180,840Q156,840 138,822Q120,804 120,780L120,180Q120,156 138,138Q156,120 180,120L780,120Q804,120 822,138Q840,156 840,180L840,780Q840,804 822,822Q804,840 780,840L180,840ZM180,780L780,780Q780,780 780,780Q780,780 780,780L780,277L180,277L180,780Q180,780 180,780Q180,780 180,780Z" />
+        android:fillColor="@android:color/black"
+        android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320L160,320L160,720Q160,720 160,720Q160,720 160,720Z"/>
 </vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml
deleted file mode 100644
index 7c4f499..0000000
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="20dp"
-    android:height="20dp"
-    android:viewportWidth="20"
-    android:viewportHeight="20">
-  <path
-      android:pathData="M15.701,14.583L18.567,17.5L17.425,18.733L14.525,15.833L12.442,17.917V12.5H17.917L15.701,14.583ZM15.833,5.833H17.5V7.5H15.833V5.833ZM17.5,4.167H15.833V2.567C16.75,2.567 17.5,3.333 17.5,4.167ZM12.5,2.5H14.167V4.167H12.5V2.5ZM15.833,9.167H17.5V10.833H15.833V9.167ZM7.5,17.5H5.833V15.833H7.5V17.5ZM4.167,7.5H2.5V5.833H4.167V7.5ZM4.167,2.567V4.167H2.5C2.5,3.333 3.333,2.567 4.167,2.567ZM4.167,14.167H2.5V12.5H4.167V14.167ZM7.5,4.167H5.833V2.5H7.5V4.167ZM10.833,4.167H9.167V2.5H10.833V4.167ZM10.833,17.5H9.167V15.833H10.833V17.5ZM4.167,10.833H2.5V9.167H4.167V10.833ZM4.167,17.567C3.25,17.567 2.5,16.667 2.5,15.833H4.167V17.567Z"
-      android:fillColor="#1C1C14"/>
-</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
index a5605a7..fa18e2b 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -80,11 +80,14 @@
 
     <com.android.wm.shell.windowdecor.MaximizeButtonView
         android:id="@+id/maximize_button_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:layout_width="44dp"
+        android:layout_height="40dp"
         android:layout_gravity="end"
+        android:layout_marginHorizontal="8dp"
+        android:paddingHorizontal="5dp"
+        android:paddingVertical="3dp"
         android:clickable="true"
-        android:focusable="true" />
+        android:focusable="true"/>
 
     <ImageButton
         android:id="@+id/close_window"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index c6f85a0..fca2fe4 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -134,15 +134,6 @@
             android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot"
             android:drawableTint="?androidprv:attr/materialColorOnSurface"
             style="@style/DesktopModeHandleMenuActionButton"/>
-
-        <Button
-            android:id="@+id/select_button"
-            android:contentDescription="@string/select_text"
-            android:text="@string/select_text"
-            android:drawableStart="@drawable/desktop_mode_ic_handle_menu_select"
-            android:drawableTint="?androidprv:attr/materialColorOnSurface"
-            style="@style/DesktopModeHandleMenuActionButton"/>
-
     </LinearLayout>
 </LinearLayout>
 
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
index e0057fe..296c8956 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -20,16 +20,16 @@
         android:id="@+id/progress_bar"
         style="?android:attr/progressBarStyleHorizontal"
         android:progressDrawable="@drawable/circular_progress"
-        android:layout_width="40dp"
-        android:layout_height="40dp"
+        android:layout_width="34dp"
+        android:layout_height="34dp"
         android:indeterminate="false"
         android:visibility="invisible"/>
 
     <ImageButton
         android:id="@+id/maximize_window"
-        android:layout_width="40dp"
-        android:layout_height="40dp"
-        android:padding="9dp"
+        android:layout_width="34dp"
+        android:layout_height="34dp"
+        android:padding="5dp"
         android:contentDescription="@string/maximize_button_text"
         android:tint="?androidprv:attr/materialColorOnSurface"
         android:background="?android:selectableItemBackgroundBorderless"
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 8fd6ffe..474430e 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
@@ -717,11 +717,6 @@
 
             // Hide the stack after a delay, if needed.
             updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
-
-            if (mShouldReorderBubblesAfterGestureCompletes) {
-                mShouldReorderBubblesAfterGestureCompletes = false;
-                updateBubbleOrderInternal(mBubbleData.getBubbles(), true);
-            }
         }
     };
 
@@ -2732,6 +2727,12 @@
                 ev.getAction() != MotionEvent.ACTION_UP
                         && ev.getAction() != MotionEvent.ACTION_CANCEL;
 
+        // If there is a deferred reorder action, and the gesture is over, run it now.
+        if (mShouldReorderBubblesAfterGestureCompletes && !mIsGestureInProgress) {
+            mShouldReorderBubblesAfterGestureCompletes = false;
+            updateBubbleOrderInternal(mBubbleData.getBubbles(), false);
+        }
+
         return dispatched;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 4d47ca9..139cde2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -983,7 +983,13 @@
         // cache current min/max size
         Point minSize = mPipBoundsState.getMinSize();
         Point maxSize = mPipBoundsState.getMaxSize();
-        mPipBoundsState.updateMinMaxSize(pictureInPictureParams.getAspectRatioFloat());
+        final float aspectRatioFloat;
+        if (pictureInPictureParams.hasSetAspectRatio()) {
+            aspectRatioFloat = pictureInPictureParams.getAspectRatioFloat();
+        } else {
+            aspectRatioFloat = mPipBoundsAlgorithm.getDefaultAspectRatio();
+        }
+        mPipBoundsState.updateMinMaxSize(aspectRatioFloat);
         final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
                 pictureInPictureParams);
         // restore min/max size, as this is referenced later in OnDisplayChangingListener and needs
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index dc9b532..bf22193 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -442,12 +442,6 @@
                 mDesktopTasksController.requestSplit(decoration.mTaskInfo);
             } else if (id == R.id.collapse_menu_button) {
                 decoration.closeHandleMenu();
-            } else if (id == R.id.select_button) {
-                if (DesktopModeStatus.IS_DISPLAY_CHANGE_ENABLED) {
-                    // TODO(b/278084491): dev option to enable display switching
-                    //  remove when select is implemented
-                    mDesktopTasksController.moveToNextDisplay(mTaskId);
-                }
             } else if (id == R.id.maximize_window) {
                 final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                 decoration.closeHandleMenu();
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 b37dd0d..3d0dd31 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
@@ -35,7 +35,6 @@
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
-import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -53,6 +52,7 @@
  */
 class HandleMenu {
     private static final String TAG = "HandleMenu";
+    private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false;
     private final Context mContext;
     private final WindowDecoration mParentDecor;
     private WindowDecoration.AdditionalWindow mHandleMenuWindow;
@@ -185,11 +185,9 @@
      * Set up interactive elements & height of handle menu's more actions pill
      */
     private void setupMoreActionsPill(View handleMenu) {
-        final Button selectBtn = handleMenu.findViewById(R.id.select_button);
-        selectBtn.setOnClickListener(mOnClickListener);
-        final Button screenshotBtn = handleMenu.findViewById(R.id.screenshot_button);
-        // TODO: Remove once implemented.
-        screenshotBtn.setVisibility(View.GONE);
+        if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
+            handleMenu.findViewById(R.id.more_actions_pill).setVisibility(View.GONE);
+        }
     }
 
     /**
@@ -305,12 +303,15 @@
      * Determines handle menu height based on if windowing pill should be shown.
      */
     private int getHandleMenuHeight(Resources resources) {
-        int menuHeight = loadDimensionPixelSize(resources,
-                R.dimen.desktop_mode_handle_menu_height);
+        int menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height);
         if (!mShouldShowWindowingPill) {
             menuHeight -= loadDimensionPixelSize(resources,
                     R.dimen.desktop_mode_handle_menu_windowing_pill_height);
         }
+        if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
+            menuHeight -= loadDimensionPixelSize(resources,
+                    R.dimen.desktop_mode_handle_menu_more_actions_pill_height);
+        }
         return menuHeight;
     }
 
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp b/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp
new file mode 100644
index 0000000..4b008a7
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp
@@ -0,0 +1,51 @@
+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_libs_androidfw_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_libs_androidfw_license"],
+}
+
+cc_fuzz {
+    name: "resxmlparser_fuzzer",
+    srcs: [
+        "resxmlparser_fuzzer.cpp",
+    ],
+    host_supported: true,
+
+    static_libs: ["libgmock"],
+    target: {
+        android: {
+            shared_libs: [
+                "libandroidfw",
+                "libbase",
+                "libbinder",
+                "libcutils",
+                "liblog",
+                "libutils",
+            ],
+        },
+        host: {
+            static_libs: [
+                "libandroidfw",
+                "libbase",
+                "libbinder",
+                "libcutils",
+                "liblog",
+                "libutils",
+            ],
+        },
+        darwin: {
+            // libbinder is not supported on mac
+            enabled: false,
+        },
+    },
+
+    include_dirs: [
+        "system/incremental_delivery/incfs/util/include/",
+    ],
+
+    corpus: ["testdata/*"],
+    dictionary: "xmlparser_fuzzer.dict",
+}
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
new file mode 100644
index 0000000..829a396
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <memory>
+#include <cstdint>
+#include <cstddef>
+#include <fuzzer/FuzzedDataProvider.h>
+#include "androidfw/ResourceTypes.h"
+
+static void populateDynamicRefTableWithFuzzedData(
+    android::DynamicRefTable& table,
+    FuzzedDataProvider& fuzzedDataProvider) {
+
+    const size_t numMappings = fuzzedDataProvider.ConsumeIntegralInRange<size_t>(1, 5);
+    for (size_t i = 0; i < numMappings; ++i) {
+        const uint8_t packageId = fuzzedDataProvider.ConsumeIntegralInRange<uint8_t>(0x02, 0x7F);
+
+        // Generate a package name
+        std::string packageName;
+        size_t packageNameLength = fuzzedDataProvider.ConsumeIntegralInRange<size_t>(1, 128);
+        for (size_t j = 0; j < packageNameLength; ++j) {
+            // Consume characters only in the ASCII range (0x20 to 0x7E) to ensure valid UTF-8
+            char ch = fuzzedDataProvider.ConsumeIntegralInRange<char>(0x20, 0x7E);
+            packageName.push_back(ch);
+        }
+
+        // Convert std::string to String16 for compatibility
+        android::String16 androidPackageName(packageName.c_str(), packageName.length());
+
+        // Add the mapping to the table
+        table.addMapping(androidPackageName, packageId);
+    }
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+    FuzzedDataProvider fuzzedDataProvider(data, size);
+
+    auto dynamic_ref_table = std::make_shared<android::DynamicRefTable>();
+
+    // Populate the DynamicRefTable with fuzzed data
+    populateDynamicRefTableWithFuzzedData(*dynamic_ref_table, fuzzedDataProvider);
+
+    auto tree = android::ResXMLTree(std::move(dynamic_ref_table));
+
+    std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>();
+    if (tree.setTo(xmlData.data(), xmlData.size()) != android::NO_ERROR) {
+        return 0; // Exit early if unable to parse XML data
+    }
+
+    tree.restart();
+
+    size_t len = 0;
+    auto code = tree.next();
+    if (code == android::ResXMLParser::START_TAG) {
+        // Access element name
+        auto name = tree.getElementName(&len);
+
+        // Access attributes of the current element
+        for (size_t i = 0; i < tree.getAttributeCount(); i++) {
+            // Access attribute name
+            auto attrName = tree.getAttributeName(i, &len);
+        }
+    } else if (code == android::ResXMLParser::TEXT) {
+        const auto text = tree.getText(&len);
+    }
+    return 0; // Non-zero return values are reserved for future use.
+}
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml
new file mode 100644
index 0000000..417fec7
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <child id="1">
+        <subchild type="A">Content A</subchild>
+        <subchild type="B">Content B</subchild>
+    </child>
+    <child id="2" extra="data">
+        <subchild type="C">Content C</subchild>
+    </child>
+</root>
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml
new file mode 100644
index 0000000..7e13db5
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <child1>Value 1</child1>
+    <child2>Value 2</child2>
+</root>
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml
new file mode 100644
index 0000000..90cdf35
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <!-- Example with special characters and CDATA -->
+    <data><![CDATA[Some <encoded> data & other "special" characters]]></data>
+    <message>Hello &amp; Welcome!</message>
+</root>
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict b/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict
new file mode 100644
index 0000000..745ded4
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict
@@ -0,0 +1,11 @@
+root_tag=<root>
+child_tag=<child>
+end_child_tag=</child>
+id_attr=id="
+type_attr=type="
+cdata_start=<![CDATA[
+cdata_end=]]>
+ampersand_entity=&amp;
+xml_header=<?xml version="1.0" encoding="UTF-8"?>
+comment_start=<!--
+comment_end= -->
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 2ea4e3f..af169f4 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -540,7 +540,7 @@
 }
 
 bool Tree::canReuseBitmap(Bitmap* bitmap, int width, int height) {
-    return bitmap && width <= bitmap->width() && height <= bitmap->height();
+    return bitmap && width == bitmap->width() && height == bitmap->height();
 }
 
 void Tree::onPropertyChanged(TreeProperties* prop) {
diff --git a/location/java/android/location/provider/ForwardGeocodeRequest.java b/location/java/android/location/provider/ForwardGeocodeRequest.java
index 8f227b1..89d14fb 100644
--- a/location/java/android/location/provider/ForwardGeocodeRequest.java
+++ b/location/java/android/location/provider/ForwardGeocodeRequest.java
@@ -260,7 +260,11 @@
             mCallingAttributionTag = null;
         }
 
-        /** Sets the attribution tag. */
+        /**
+         * Sets the attribution tag.
+         *
+         * @param attributionTag The attribution tag to associate with the request.
+         */
         @NonNull
         public Builder setCallingAttributionTag(@NonNull String attributionTag) {
             mCallingAttributionTag = attributionTag;
diff --git a/location/java/android/location/provider/ReverseGeocodeRequest.java b/location/java/android/location/provider/ReverseGeocodeRequest.java
index 57c9047..2107707 100644
--- a/location/java/android/location/provider/ReverseGeocodeRequest.java
+++ b/location/java/android/location/provider/ReverseGeocodeRequest.java
@@ -207,7 +207,11 @@
             mCallingAttributionTag = null;
         }
 
-        /** Sets the attribution tag. */
+        /**
+         * Sets the attribution tag.
+         *
+         * @param attributionTag The attribution tag to associate with the request.
+         */
         @NonNull
         public Builder setCallingAttributionTag(@NonNull String attributionTag) {
             mCallingAttributionTag = attributionTag;
diff --git a/media/java/android/media/metrics/PlaybackSession.java b/media/java/android/media/metrics/PlaybackSession.java
index f8dd756..6223acf 100644
--- a/media/java/android/media/metrics/PlaybackSession.java
+++ b/media/java/android/media/metrics/PlaybackSession.java
@@ -24,7 +24,10 @@
 import java.util.Objects;
 
 /**
- * An instances of this class represents a session of media playback.
+ * An instance of this class represents a session of media playback used to report playback
+ * metrics and events.
+ *
+ * Create a new instance using {@link MediaMetricsManager#createPlaybackSession}.
  */
 public final class PlaybackSession implements AutoCloseable {
     private final @NonNull String mId;
@@ -80,6 +83,21 @@
         mManager.reportTrackChangeEvent(mId, event);
     }
 
+    /**
+     * A session ID is used to identify a unique playback and to tie together lower-level
+     * playback components.
+     *
+     * Associate this session with a {@link MediaCodec} by passing the ID into
+     * {@link MediaFormat} through {@link MediaFormat#LOG_SESSION_ID} when
+     * creating the {@link MediaCodec}.
+     *
+     * Associate this session with an {@link AudioTrack} by calling
+     * {@link AudioTrack#setLogSessionId}.
+     *
+     * Associate this session with {@link MediaDrm} and {@link MediaCrypto} by calling
+     * {@link MediaDrm#getPlaybackComponent} and then calling
+     * {@link PlaybackComponent#setLogSessionId}.
+     */
     public @NonNull LogSessionId getSessionId() {
         return mLogSessionId;
     }
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 8396005..0fc80dd 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2895,6 +2895,10 @@
         jint offset,
         jint size,
         std::shared_ptr<C2Buffer> *buffer) {
+    if ((offset + size) > context->capacity()) {
+        ALOGW("extractBufferFromContext: offset + size provided exceed capacity");
+        return;
+    }
     *buffer = context->toC2Buffer(offset, size);
     if (*buffer == nullptr) {
         if (!context->mMemory) {
@@ -2995,18 +2999,15 @@
                     "MediaCodec.LinearBlock#obtain method to obtain a compatible buffer.");
             return;
         }
-        sp<CryptoInfosWrapper> cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()};
-        jint sampleSize = 0;
+        sp<CryptoInfosWrapper> cryptoInfos = nullptr;
+        jint sampleSize = totalSize;
         if (cryptoInfoArray != nullptr) {
+            cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()};
             extractCryptoInfosFromObjectArray(env,
                     &sampleSize,
                     &cryptoInfos->value,
                     cryptoInfoArray,
                     &errorDetailMsg);
-        } else {
-            sampleSize = totalSize;
-            std::unique_ptr<CodecCryptoInfo> cryptoInfo{new MediaCodecCryptoInfo(totalSize)};
-            cryptoInfos->value.push_back(std::move(cryptoInfo));
         }
         if (env->ExceptionCheck()) {
             // Creation of cryptoInfo failed. Let the exception bubble up.
diff --git a/native/android/Android.bp b/native/android/Android.bp
index 7f3792d..752ebdf 100644
--- a/native/android/Android.bp
+++ b/native/android/Android.bp
@@ -98,6 +98,7 @@
         "libpowermanager",
         "android.hardware.configstore@1.0",
         "android.hardware.configstore-utils",
+        "android.os.flags-aconfig-cc",
         "libnativedisplay",
     ],
 
diff --git a/native/android/input.cpp b/native/android/input.cpp
index 53699bc..0a22314 100644
--- a/native/android/input.cpp
+++ b/native/android/input.cpp
@@ -124,11 +124,11 @@
 }
 
 float AMotionEvent_getXOffset(const AInputEvent* motion_event) {
-    return static_cast<const MotionEvent*>(motion_event)->getXOffset();
+    return static_cast<const MotionEvent*>(motion_event)->getRawXOffset();
 }
 
 float AMotionEvent_getYOffset(const AInputEvent* motion_event) {
-    return static_cast<const MotionEvent*>(motion_event)->getYOffset();
+    return static_cast<const MotionEvent*>(motion_event)->getRawYOffset();
 }
 
 float AMotionEvent_getXPrecision(const AInputEvent* motion_event) {
diff --git a/nfc/Android.bp b/nfc/Android.bp
index b6bc40d..7dd16ba 100644
--- a/nfc/Android.bp
+++ b/nfc/Android.bp
@@ -38,6 +38,7 @@
     name: "framework-nfc",
     libs: [
         "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage
+        "framework-permission-s",
     ],
     static_libs: [
         "android.nfc.flags-aconfig-java",
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 9d0221a..54f1421 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -194,13 +194,13 @@
 package android.nfc.cardemulation {
 
   public final class CardEmulation {
-    method public boolean categoryAllowsForegroundPreference(String);
+    method @Deprecated public boolean categoryAllowsForegroundPreference(String);
     method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public java.util.List<java.lang.String> getAidsForPreferredPaymentService();
     method public java.util.List<java.lang.String> getAidsForService(android.content.ComponentName, String);
-    method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public CharSequence getDescriptionForPreferredPaymentService();
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public CharSequence getDescriptionForPreferredPaymentService();
     method public static android.nfc.cardemulation.CardEmulation getInstance(android.nfc.NfcAdapter);
-    method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public String getRouteDestinationForPreferredPaymentService();
-    method public int getSelectionModeForCategory(String);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public String getRouteDestinationForPreferredPaymentService();
+    method @Deprecated public int getSelectionModeForCategory(String);
     method public boolean isDefaultServiceForAid(android.content.ComponentName, String);
     method public boolean isDefaultServiceForCategory(android.content.ComponentName, String);
     method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>);
@@ -268,9 +268,9 @@
     ctor public PollingFrame(int, @Nullable byte[], int, int);
     method public int describeContents();
     method @NonNull public byte[] getData();
-    method public int getGain();
     method public int getTimestamp();
     method public int getType();
+    method public int getVendorSpecificGain();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.PollingFrame> CREATOR;
     field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final int POLLING_LOOP_TYPE_A = 65; // 0x41
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index ea58504..e55f540 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -27,6 +27,7 @@
 import android.annotation.UserHandleAware;
 import android.annotation.UserIdInt;
 import android.app.Activity;
+import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -278,13 +279,22 @@
      * @param category The category, e.g. {@link #CATEGORY_PAYMENT}
      * @return whether AIDs in the category can be handled by a service
      *         specified by the foreground app.
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will be done in a AID
+     * category agnostic manner.
      */
     @SuppressWarnings("NonUserGetterCalled")
+    @Deprecated
     public boolean categoryAllowsForegroundPreference(String category) {
+        Context contextAsUser = mContext.createContextAsUser(
+                UserHandle.of(UserHandle.myUserId()), 0);
+        RoleManager roleManager = contextAsUser.getSystemService(RoleManager.class);
+        if (roleManager.isRoleAvailable(RoleManager.ROLE_WALLET)) {
+            return true;
+        }
         if (CATEGORY_PAYMENT.equals(category)) {
             boolean preferForeground = false;
-            Context contextAsUser = mContext.createContextAsUser(
-                    UserHandle.of(UserHandle.myUserId()), 0);
             try {
                 preferForeground = Settings.Secure.getInt(
                         contextAsUser.getContentResolver(),
@@ -309,7 +319,12 @@
      *    to pick a service if there is a conflict.
      * @param category The category, for example {@link #CATEGORY_PAYMENT}
      * @return the selection mode for the passed in category
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will be done in a AID
+     * category agnostic manner.
      */
+    @Deprecated
     public int getSelectionModeForCategory(String category) {
         if (CATEGORY_PAYMENT.equals(category)) {
             boolean paymentRegistered = false;
@@ -792,8 +807,15 @@
      *                                               (e.g. eSE/eSE1, eSE2, etc.).
      *                          2. "OffHost" if the payment service does not specify secure element
      *                             name.
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will go to the Wallet Holder app.
+     * A payment service will be selected automatically based on registered AIDs. In the case of
+     * multiple services that register for the same payment AID, the selection will be done on
+     * an alphabetical order based on the component names.
      */
     @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+    @Deprecated
     @Nullable
     public String getRouteDestinationForPreferredPaymentService() {
         try {
@@ -836,8 +858,15 @@
      * Returns a user-visible description of the preferred payment service.
      *
      * @return the preferred payment service description
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will go to the Wallet Holder app.
+     * A payment service will be selected automatically based on registered AIDs. In the case of
+     * multiple services that register for the same payment AID, the selection will be done on
+     * an alphabetical order based on the component names.
      */
     @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+    @Deprecated
     @Nullable
     public CharSequence getDescriptionForPreferredPaymentService() {
         try {
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index 994f4ae..29d7bdf 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -157,7 +157,7 @@
         mType = frame.getInt(KEY_POLLING_LOOP_TYPE);
         byte[] data = frame.getByteArray(KEY_POLLING_LOOP_DATA);
         mData = (data == null) ? new byte[0] : data;
-        mGain = frame.getByte(KEY_POLLING_LOOP_GAIN);
+        mGain = frame.getInt(KEY_POLLING_LOOP_GAIN, -1);
         mTimestamp = frame.getInt(KEY_POLLING_LOOP_TIMESTAMP);
     }
 
@@ -194,8 +194,9 @@
     /**
      * Returns the gain representing the field strength of the NFC field when this polling loop
      * frame was observed.
+     * @return the gain or -1 if there is no gain measurement associated with this frame.
      */
-    public int getGain() {
+    public int getVendorSpecificGain() {
         return mGain;
     }
 
@@ -227,7 +228,9 @@
     public Bundle toBundle() {
         Bundle frame = new Bundle();
         frame.putInt(KEY_POLLING_LOOP_TYPE, getType());
-        frame.putByte(KEY_POLLING_LOOP_GAIN, (byte) getGain());
+        if (getVendorSpecificGain() != -1) {
+            frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain());
+        }
         frame.putByteArray(KEY_POLLING_LOOP_DATA, getData());
         frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
         return frame;
@@ -236,7 +239,7 @@
     @Override
     public String toString() {
         return "PollingFrame { Type: " + (char) getType()
-                + ", gain: " + getGain()
+                + ", gain: " + getVendorSpecificGain()
                 + ", timestamp: " + Integer.toUnsignedString(getTimestamp())
                 + ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }";
     }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 4e1f4ee..d7a2e36 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -21,14 +21,13 @@
 import android.content.Context
 import android.credentials.CredentialManager
 import android.credentials.GetCredentialRequest
-import android.credentials.GetCredentialResponse
-import android.credentials.GetCredentialException
 import android.credentials.GetCandidateCredentialsResponse
 import android.credentials.GetCandidateCredentialsException
 import android.credentials.CredentialOption
 import android.credentials.selection.Entry
 import android.credentials.selection.GetCredentialProviderData
 import android.credentials.selection.ProviderData
+import android.graphics.BlendMode
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.os.CancellationSignal
@@ -123,13 +122,10 @@
         // TODO(b/324635774): Use callback for validating. If the request is coming
         // directly from the view, there should be a corresponding callback, otherwise
         // we should fail fast,
-        val getCredCallback = getCredManCallback(structure)
         if (getCredRequest == null) {
             Log.i(TAG, "No credential manager request found")
             callback.onFailure("No credential manager request found")
             return
-        } else if (getCredCallback == null) {
-            Log.i(TAG, "No credential manager callback found")
         }
         val credentialManager: CredentialManager =
                 getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
@@ -353,6 +349,7 @@
         val sliceBuilder = InlineSuggestionUi
                 .newContentBuilder(pendingIntent)
                 .setTitle(displayName)
+        icon.setTintBlendMode(BlendMode.DST)
         sliceBuilder.setStartIcon(icon)
         if (primaryEntry.credentialType ==
                 CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) {
@@ -526,42 +523,6 @@
         TODO("Not yet implemented")
     }
 
-    private fun getCredManCallback(structure: AssistStructure): OutcomeReceiver<
-            GetCredentialResponse, GetCredentialException>? {
-        return traverseStructureForCallback(structure)
-    }
-
-    private fun traverseStructureForCallback(
-            structure: AssistStructure
-    ): OutcomeReceiver<GetCredentialResponse, GetCredentialException>? {
-        val windowNodes: List<AssistStructure.WindowNode> =
-                structure.run {
-                    (0 until windowNodeCount).map { getWindowNodeAt(it) }
-                }
-
-        windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
-            return traverseNodeForCallback(windowNode.rootViewNode)
-        }
-        return null
-    }
-
-    private fun traverseNodeForCallback(
-            viewNode: AssistStructure.ViewNode
-    ): OutcomeReceiver<GetCredentialResponse, GetCredentialException>? {
-        val children: List<AssistStructure.ViewNode> =
-                viewNode.run {
-                    (0 until childCount).map { getChildAt(it) }
-                }
-
-        children.forEach { childNode: AssistStructure.ViewNode ->
-            if (childNode.isFocused() && childNode.credentialManagerCallback != null) {
-                return childNode.credentialManagerCallback
-            }
-            return traverseNodeForCallback(childNode)
-        }
-        return null
-    }
-
     private fun getCredManRequest(
             structure: AssistStructure,
             sessionId: Int,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
index a7b5c36..e43b09e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
@@ -39,7 +39,6 @@
 import com.android.credentialmanager.ui.theme.EntryShape
 import kotlinx.coroutines.launch
 
-
 /** Draws a modal bottom sheet with the same styles and effects shared by various flows. */
 @Composable
 @OptIn(ExperimentalMaterial3Api::class)
@@ -73,7 +72,7 @@
                 dragHandle = null,
                 // Never take over the full screen. We always want to leave some top scrim space
                 // for exiting and viewing the underlying app to help a user gain context.
-                modifier = Modifier.padding(top = 56.dp),
+                modifier = Modifier.padding(top = 72.dp),
         )
     } else {
         val scope = rememberCoroutineScope()
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
index c68ae8b..006a2d9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
@@ -16,6 +16,7 @@
 
 package com.android.credentialmanager.common.ui
 
+import android.credentials.flags.Flags
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.WindowInsets
@@ -63,7 +64,7 @@
             modifier = Modifier.padding(
                 start = 24.dp,
                 end = 24.dp,
-                bottom = 18.dp,
+                bottom = if (Flags.selectorUiImprovementsEnabled()) 8.dp else 18.dp,
                 top = if (topAppBar == null) 24.dp else 0.dp
             ).fillMaxWidth().wrapContentHeight(),
             horizontalAlignment = Alignment.CenterHorizontally,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index 56bd066..965ee86 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -152,14 +152,14 @@
                                 },
                             )
                         }
-                    } else if (entrySecondLineText != null) {
+                    } else if (!entrySecondLineText.isNullOrBlank()) {
                         BodySmallText(
                             text = entrySecondLineText,
                             enforceOneLine = enforceOneLine,
                             onTextLayout = onTextLayout,
                         )
                     }
-                    if (entryThirdLineText != null) {
+                    if (!entryThirdLineText.isNullOrBlank()) {
                         BodySmallText(
                             text = entryThirdLineText,
                             enforceOneLine = enforceOneLine,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
index 3ebdd20..ff421bc 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
@@ -21,63 +21,20 @@
 import android.content.Context
 import android.util.Size
 import android.widget.inline.InlinePresentationSpec
-import androidx.autofill.inline.common.TextViewStyle
-import androidx.autofill.inline.common.ViewStyle
-import androidx.autofill.inline.UiVersions
-import androidx.autofill.inline.UiVersions.Style
-import androidx.autofill.inline.v1.InlineSuggestionUi
-import androidx.core.content.ContextCompat
-import android.util.TypedValue
-import android.graphics.Typeface
-
 
 class InlinePresentationsFactory {
     companion object {
-        private const val googleSansMediumFontFamily = "google-sans-medium"
-        private const val googleSansTextFontFamily = "google-sans-text"
-        // There is no min width required for now but this is needed for the spec builder
-        private const val minInlineWidth = 5000
+        // There is no max width required for now but this is needed for the spec builder
+        private const val maxInlineWidth = 5000
 
 
         fun modifyInlinePresentationSpec(context: Context,
                                          originalSpec: InlinePresentationSpec): InlinePresentationSpec {
             return InlinePresentationSpec.Builder(Size(originalSpec.minSize.width, originalSpec
                     .minSize.height),
-                    Size(minInlineWidth, originalSpec
+                    Size(maxInlineWidth, originalSpec
                             .maxSize.height))
-                    .setStyle(UiVersions.newStylesBuilder().addStyle(getStyle(context)).build())
-                    .build()
-        }
-
-
-        fun getStyle(context: Context): Style {
-            val textColorPrimary = ContextCompat.getColor(context,
-                    com.android.credentialmanager.R.color.text_primary)
-            val textColorSecondary = ContextCompat.getColor(context,
-                    com.android.credentialmanager.R.color.text_secondary)
-            val textColorBackground = ContextCompat.getColor(context,
-                    com.android.credentialmanager.R.color.inline_background)
-            val chipHorizontalPadding = context.resources.getDimensionPixelSize(com.android
-                    .credentialmanager.R.dimen.horizontal_chip_padding)
-            val chipVerticalPadding = context.resources.getDimensionPixelSize(com.android
-                    .credentialmanager.R.dimen.vertical_chip_padding)
-            return InlineSuggestionUi.newStyleBuilder()
-                    .setChipStyle(
-                            ViewStyle.Builder().setPadding(chipHorizontalPadding,
-                                    chipVerticalPadding,
-                                    chipHorizontalPadding, chipVerticalPadding).build()
-                    )
-                    .setTitleStyle(
-                            TextViewStyle.Builder().setTextColor(textColorPrimary).setTextSize
-                            (TypedValue.COMPLEX_UNIT_DIP, 14F)
-                                    .setTypeface(googleSansMediumFontFamily,
-                                            Typeface.NORMAL).setBackgroundColor(textColorBackground)
-                                    .build()
-                    )
-                    .setSubtitleStyle(TextViewStyle.Builder().setTextColor(textColorSecondary)
-                            .setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12F).setTypeface
-                            (googleSansTextFontFamily, Typeface.NORMAL).setBackgroundColor
-                            (textColorBackground).build())
+                    .setStyle(originalSpec.getStyle())
                     .build()
         }
     }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 4ef7760..af78573 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -336,7 +336,7 @@
             if (!footerDescription.isNullOrBlank()) {
                 item {
                     Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
-                        BodySmallText(text = footerDescription)
+                        BodyMediumText(text = footerDescription)
                     }
                 }
                 item { Divider(thickness = 24.dp, color = Color.Transparent) }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 748c798..bc0ea02 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -786,16 +786,16 @@
         else if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in_24)
         else null,
         entryHeadlineText = username,
-        entrySecondLineText =
+        entrySecondLineText = displayName,
+        entryThirdLineText =
         (if (hasSingleEntry != null && hasSingleEntry)
             if (credentialEntryInfo.credentialType == CredentialType.PASSKEY ||
                     credentialEntryInfo.credentialType == CredentialType.PASSWORD)
-                listOf(displayName)
+                emptyList()
             // Still show the type display name for all non-password/passkey types since it won't be
             // mentioned in the bottom sheet heading.
-            else listOf(displayName, credentialEntryInfo.credentialTypeDisplayName)
+            else listOf(credentialEntryInfo.credentialTypeDisplayName)
         else listOf(
-                displayName,
                 credentialEntryInfo.credentialTypeDisplayName,
                 credentialEntryInfo.providerDisplayName
         )).filterNot(TextUtils::isEmpty).let { itemsToDisplay ->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index ef40188..e35acae 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -25,6 +25,7 @@
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.model.get.RemoteEntryInfo
 import com.android.internal.util.Preconditions
+import java.time.Instant
 
 data class GetCredentialUiState(
     val isRequestForAllOptions: Boolean,
@@ -156,11 +157,17 @@
     userNameToCredentialEntryMap.values.forEach {
         it.sortWith(comparator)
     }
-    // Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
+    // Transform to list of PerUserNameCredentialEntryLists and then sort the outer list (of
+    // entries grouped by username / entryGroupId) based on the latest timestamp within that
+    // PerUserNameCredentialEntryList
     val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
         PerUserNameCredentialEntryList(it.key, it.value)
     }.sortedWith(
-        compareByDescending { it.sortedCredentialEntryList.first().lastUsedTimeMillis }
+        compareByDescending {
+            it.sortedCredentialEntryList.maxByOrNull{ entry ->
+                entry.lastUsedTimeMillis ?: Instant.MIN
+            }?.lastUsedTimeMillis ?: Instant.MIN
+        }
     )
 
     return ProviderDisplayInfo(
@@ -211,7 +218,7 @@
         val typePriorityMap: Map<String, Int>,
 ) : Comparator<CredentialEntryInfo> {
     override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
-        // First prefer passkey type for its security benefits
+        // First rank by priorities of each credential type.
         if (p0.rawCredentialType != p1.rawCredentialType) {
             val p0Priority = typePriorityMap.getOrDefault(
                     p0.rawCredentialType, PriorityHints.PRIORITY_DEFAULT
@@ -225,6 +232,7 @@
                 return 1
             }
         }
+        // Then rank by last used timestamps.
         val p0LastUsedTimeMillis = p0.lastUsedTimeMillis
         val p1LastUsedTimeMillis = p1.lastUsedTimeMillis
         // Then order by last used timestamp
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 98a5a67..79c810c 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -54,6 +54,7 @@
         "androidx.lifecycle_lifecycle-extensions",
         "android.content.pm.flags-aconfig-java",
         "android.os.flags-aconfig-java",
+        "android.multiuser.flags-aconfig-java",
     ],
 
     lint: {
@@ -85,6 +86,7 @@
         "androidx.lifecycle_lifecycle-extensions",
         "android.content.pm.flags-aconfig-java",
         "android.os.flags-aconfig-java",
+        "android.multiuser.flags-aconfig-java",
     ],
     aaptflags: ["--product tablet"],
 
@@ -118,6 +120,7 @@
         "androidx.lifecycle_lifecycle-extensions",
         "android.content.pm.flags-aconfig-java",
         "android.os.flags-aconfig-java",
+        "android.multiuser.flags-aconfig-java",
     ],
     aaptflags: ["--product tv"],
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
index 634e067..cf2f85e 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
@@ -20,7 +20,6 @@
 
 import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
 
-import android.Manifest;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -28,10 +27,10 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.content.pm.Flags;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.res.AssetFileDescriptor;
+import android.Manifest;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -201,7 +200,7 @@
         params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
                 PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);
 
-        if (pfd != null && Flags.readInstallInfo()) {
+        if (pfd != null) {
             try {
                 final PackageInstaller.InstallInfo result = installer.readInstallInfo(pfd,
                         debugPathName, 0);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index e95a8e6..45bfe54 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -31,7 +31,6 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.Flags;
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
@@ -400,10 +399,7 @@
             final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
                     -1 /* defaultValue */);
             final SessionInfo info = mInstaller.getSessionInfo(sessionId);
-            String resolvedPath = null;
-            if (info != null && Flags.getResolvedApkPath()) {
-                resolvedPath = info.getResolvedBaseApkPath();
-            }
+            String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null;
             if (info == null || !info.isSealed() || resolvedPath == null) {
                 Log.w(TAG, "Session " + sessionId + " in funky state; ignoring");
                 finish();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
index 221ca4f..8f5d07c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
@@ -166,6 +166,7 @@
                     messageBuilder.append(getString(
                             R.string.uninstall_application_text_current_user_clone_profile));
                 } else if (Flags.allowPrivateProfile()
+                        && android.multiuser.Flags.enablePrivateSpaceFeatures()
                         && customUserManager.isPrivateProfile()
                         && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) {
                     messageBuilder.append(
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index 22caabd..aeabbd5 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -25,7 +25,6 @@
 import android.content.Context
 import android.content.Intent
 import android.content.pm.ApplicationInfo
-import android.content.pm.Flags
 import android.content.pm.PackageInfo
 import android.content.pm.PackageInstaller
 import android.content.pm.PackageInstaller.SessionInfo
@@ -363,7 +362,7 @@
         params.setPermissionState(
             Manifest.permission.USE_FULL_SCREEN_INTENT, SessionParams.PERMISSION_STATE_DENIED
         )
-        if (pfd != null && Flags.readInstallInfo()) {
+        if (pfd != null) {
             try {
                 val installInfo = packageInstaller.readInstallInfo(pfd, debugPathName, 0)
                 params.setAppPackageName(installInfo.packageName)
@@ -426,8 +425,7 @@
 
         if (PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action) {
             val info = packageInstaller.getSessionInfo(sessionId)
-            val resolvedPath =
-                    if (Flags.getResolvedApkPath()) info?.resolvedBaseApkPath else null
+            val resolvedPath = info?.resolvedBaseApkPath
             if (info == null || !info.isSealed || resolvedPath == null) {
                 Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring")
                 return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
index 0fc1845..c6b6d36 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
@@ -235,7 +235,9 @@
                     messageString = context.getString(
                             R.string.uninstall_application_text_current_user_clone_profile
                     )
-                } else if (Flags.allowPrivateProfile() && customUserManager!!.isPrivateProfile()) {
+                } else if (Flags.allowPrivateProfile()
+                        && android.multiuser.Flags.enablePrivateSpaceFeatures()
+                        && customUserManager!!.isPrivateProfile()) {
                     // TODO(b/324244123): Get these Strings from a User Property API.
                     messageString = context.getString(
                             R.string.uninstall_application_text_current_user_private_profile
diff --git a/packages/SettingsLib/DataStore/tests/Android.bp b/packages/SettingsLib/DataStore/tests/Android.bp
new file mode 100644
index 0000000..8770dfa
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/Android.bp
@@ -0,0 +1,24 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_app {
+    name: "SettingsLibDataStoreShell",
+    platform_apis: true,
+}
+
+android_robolectric_test {
+    name: "SettingsLibDataStoreTest",
+    srcs: ["src/**/*"],
+    static_libs: [
+        "SettingsLibDataStore",
+        "androidx.test.ext.junit",
+        "guava",
+        "mockito-robolectric-prebuilt", // mockito deps order matters!
+        "mockito-kotlin2",
+    ],
+    java_resource_dirs: ["config"],
+    instrumentation_for: "SettingsLibDataStoreShell",
+    coverage_libs: ["SettingsLibDataStore"],
+    upstream: true,
+}
diff --git a/packages/SettingsLib/DataStore/tests/AndroidManifest.xml b/packages/SettingsLib/DataStore/tests/AndroidManifest.xml
new file mode 100644
index 0000000..ffc24e4
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.datastore.test">
+
+    <application android:debuggable="true" />
+</manifest>
diff --git a/packages/SettingsLib/DataStore/tests/config/robolectric.properties b/packages/SettingsLib/DataStore/tests/config/robolectric.properties
new file mode 100644
index 0000000..fab7251
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/config/robolectric.properties
@@ -0,0 +1 @@
+sdk=NEWEST_SDK
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
new file mode 100644
index 0000000..bb791dc
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert.assertThrows
+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.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class ObserverTest {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var observer1: Observer
+
+    @Mock private lateinit var observer2: Observer
+
+    @Mock private lateinit var executor: Executor
+
+    private val observable = DataObservable()
+
+    @Test
+    fun addObserver_sameExecutor() {
+        observable.addObserver(observer1, executor)
+        observable.addObserver(observer1, executor)
+    }
+
+    @Test
+    fun addObserver_differentExecutor() {
+        observable.addObserver(observer1, executor)
+        assertThrows(IllegalStateException::class.java) {
+            observable.addObserver(observer1, MoreExecutors.directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var observer: Observer? = Observer { counter.incrementAndGet() }
+        observable.addObserver(observer!!, MoreExecutors.directExecutor())
+
+        observable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        @Suppress("unused")
+        observer = null
+        System.gc()
+        System.runFinalization()
+
+        observable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_notifyObservers_removeObserver() {
+        observable.addObserver(observer1, MoreExecutors.directExecutor())
+        observable.addObserver(observer2, executor)
+
+        observable.notifyChange(ChangeReason.DELETE)
+
+        verify(observer1).onChanged(ChangeReason.DELETE)
+        verify(observer2, never()).onChanged(any())
+        verify(executor).execute(any())
+
+        reset(observer1, executor)
+        observable.removeObserver(observer2)
+
+        observable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onChanged(ChangeReason.UPDATE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun notifyChange_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        observable.addObserver(
+            { observable.addObserver(observer1, executor) },
+            MoreExecutors.directExecutor()
+        )
+        observable.notifyChange(ChangeReason.UPDATE)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
index fc8de80..0a469b8 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
@@ -47,7 +47,7 @@
                 onCheckedChange = model.onCheckedChange,
                 paddingStart = 20.dp,
                 paddingEnd = 20.dp,
-                paddingVertical = 18.dp,
+                paddingVertical = 24.dp,
             )
         }
     }
@@ -55,7 +55,7 @@
 
 @Preview
 @Composable
-fun MainSwitchPreferencePreview() {
+private fun MainSwitchPreferencePreview() {
     SettingsTheme {
         Column {
             MainSwitchPreference(object : SwitchPreferenceModel {
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index e489bc5..2889ce2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1703,6 +1703,7 @@
 
         public boolean isPrivateProfile() {
             return android.os.Flags.allowPrivateProfile()
+                    && android.multiuser.Flags.enablePrivateSpaceFeatures()
                     && UserManager.USER_TYPE_PROFILE_PRIVATE.equals(mProfileType);
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
index da1fd55..0c7d6f0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -311,7 +311,7 @@
         }
 
         builder.apply {
-            setSourceDevice(device, sourceAddrType)
+            setSourceDevice(device, addrType)
             setSourceAdvertisingSid(sourceAdvertiserSid)
             setBroadcastId(broadcastId)
             setBroadcastName(broadcastName)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 5f026c4..e34c50e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -44,7 +44,6 @@
 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
 
 import android.annotation.TargetApi;
-import android.app.Notification;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.content.ComponentName;
@@ -67,6 +66,7 @@
 import com.android.settingslib.media.flags.Flags;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
@@ -81,11 +81,43 @@
 
 /** InfoMediaManager provide interface to get InfoMediaDevice list. */
 @RequiresApi(Build.VERSION_CODES.R)
-public abstract class InfoMediaManager extends MediaManager {
+public abstract class InfoMediaManager {
+    /** Callback for notifying device is added, removed and attributes changed. */
+    public interface MediaDeviceCallback {
 
-    private static final String TAG = "InfoMediaManager";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+        /**
+         * Callback for notifying MediaDevice list is added.
+         *
+         * @param devices the MediaDevice list
+         */
+        void onDeviceListAdded(@NonNull List<MediaDevice> devices);
+
+        /**
+         * Callback for notifying MediaDevice list is removed.
+         *
+         * @param devices the MediaDevice list
+         */
+        void onDeviceListRemoved(@NonNull List<MediaDevice> devices);
+
+        /**
+         * Callback for notifying connected MediaDevice is changed.
+         *
+         * @param id the id of MediaDevice
+         */
+        void onConnectedDeviceChanged(@Nullable String id);
+
+        /**
+         * Callback for notifying that transferring is failed.
+         *
+         * @param reason the reason that the request has failed. Can be one of followings: {@link
+         *     android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_REJECTED}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
+         */
+        void onRequestFailed(int reason);
+    }
 
     /** Checked exception that signals the specified package is not present in the system. */
     public static class PackageNotAvailableException extends Exception {
@@ -94,19 +126,22 @@
         }
     }
 
+    private static final String TAG = "InfoMediaManager";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+    @NonNull protected final Context mContext;
     @NonNull protected final String mPackageName;
+    private final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
     private MediaDevice mCurrentConnectedDevice;
     private final LocalBluetoothManager mBluetoothManager;
     private final Map<String, RouteListingPreference.Item> mPreferenceItemMap =
             new ConcurrentHashMap<>();
 
     /* package */ InfoMediaManager(
-            Context context,
+            @NonNull Context context,
             @NonNull String packageName,
-            Notification notification,
-            LocalBluetoothManager localBluetoothManager) {
-        super(context, notification);
-
+            @NonNull LocalBluetoothManager localBluetoothManager) {
+        mContext = context;
         mBluetoothManager = localBluetoothManager;
         mPackageName = packageName;
     }
@@ -115,7 +150,6 @@
     public static InfoMediaManager createInstance(
             Context context,
             @Nullable String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager) {
 
         // The caller is only interested in system routes (headsets, built-in speakers, etc), and is
@@ -127,17 +161,14 @@
 
         if (Flags.useMediaRouter2ForInfoMediaManager()) {
             try {
-                return new RouterInfoMediaManager(
-                        context, packageName, notification, localBluetoothManager);
+                return new RouterInfoMediaManager(context, packageName, localBluetoothManager);
             } catch (PackageNotAvailableException ex) {
                 // TODO: b/293578081 - Propagate this exception to callers for proper handling.
                 Log.w(TAG, "Returning a no-op InfoMediaManager for package " + packageName);
-                return new NoOpInfoMediaManager(
-                        context, packageName, notification, localBluetoothManager);
+                return new NoOpInfoMediaManager(context, packageName, localBluetoothManager);
             }
         } else {
-            return new ManagerInfoMediaManager(
-                    context, packageName, notification, localBluetoothManager);
+            return new ManagerInfoMediaManager(context, packageName, localBluetoothManager);
         }
     }
 
@@ -239,6 +270,38 @@
         return null;
     }
 
+    protected final void registerCallback(MediaDeviceCallback callback) {
+        if (!mCallbacks.contains(callback)) {
+            mCallbacks.add(callback);
+        }
+    }
+
+    protected final void unregisterCallback(MediaDeviceCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    private void dispatchDeviceListAdded(@NonNull List<MediaDevice> devices) {
+        for (MediaDeviceCallback callback : getCallbacks()) {
+            callback.onDeviceListAdded(new ArrayList<>(devices));
+        }
+    }
+
+    private void dispatchConnectedDeviceChanged(String id) {
+        for (MediaDeviceCallback callback : getCallbacks()) {
+            callback.onConnectedDeviceChanged(id);
+        }
+    }
+
+    protected void dispatchOnRequestFailed(int reason) {
+        for (MediaDeviceCallback callback : getCallbacks()) {
+            callback.onRequestFailed(reason);
+        }
+    }
+
+    private Collection<MediaDeviceCallback> getCallbacks() {
+        return new CopyOnWriteArrayList<>(mCallbacks);
+    }
+
     /**
      * Get current device that played media.
      * @return MediaDevice
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index 5925492..63056b6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -138,8 +138,7 @@
         }
 
         mInfoMediaManager =
-                InfoMediaManager.createInstance(
-                        context, packageName, notification, mLocalBluetoothManager);
+                InfoMediaManager.createInstance(context, packageName, mLocalBluetoothManager);
     }
 
     /**
@@ -505,9 +504,9 @@
         return new CopyOnWriteArrayList<>(mCallbacks);
     }
 
-    class MediaDeviceCallback implements MediaManager.MediaDeviceCallback {
+    class MediaDeviceCallback implements InfoMediaManager.MediaDeviceCallback {
         @Override
-        public void onDeviceListAdded(List<MediaDevice> devices) {
+        public void onDeviceListAdded(@NonNull List<MediaDevice> devices) {
             synchronized (mMediaDevicesLock) {
                 mMediaDevices.clear();
                 mMediaDevices.addAll(devices);
@@ -637,7 +636,7 @@
         }
 
         @Override
-        public void onDeviceListRemoved(List<MediaDevice> devices) {
+        public void onDeviceListRemoved(@NonNull List<MediaDevice> devices) {
             synchronized (mMediaDevicesLock) {
                 mMediaDevices.removeAll(devices);
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
index 453e807..c4fac35 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.media;
 
-import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
@@ -54,9 +53,8 @@
     /* package */ ManagerInfoMediaManager(
             Context context,
             @NonNull String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager) {
-        super(context, packageName, notification, localBluetoothManager);
+        super(context, packageName, localBluetoothManager);
 
         mRouterManager = MediaRouter2Manager.getInstance(context);
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
deleted file mode 100644
index d562c8a..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.settingslib.media;
-
-import android.annotation.NonNull;
-import android.app.Notification;
-import android.content.Context;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * MediaManager provide interface to get MediaDevice list.
- */
-public abstract class MediaManager {
-
-    protected final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
-
-    protected Context mContext;
-    protected Notification mNotification;
-
-    MediaManager(Context context, Notification notification) {
-        mContext = context;
-        mNotification = notification;
-    }
-
-    protected void registerCallback(MediaDeviceCallback callback) {
-        if (!mCallbacks.contains(callback)) {
-            mCallbacks.add(callback);
-        }
-    }
-
-    protected void unregisterCallback(MediaDeviceCallback callback) {
-        if (mCallbacks.contains(callback)) {
-            mCallbacks.remove(callback);
-        }
-    }
-
-    protected void dispatchDeviceListAdded(@NonNull List<MediaDevice> devices) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onDeviceListAdded(new ArrayList<>(devices));
-        }
-    }
-
-    protected void dispatchDeviceListRemoved(List<MediaDevice> devices) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onDeviceListRemoved(devices);
-        }
-    }
-
-    protected void dispatchConnectedDeviceChanged(String id) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onConnectedDeviceChanged(id);
-        }
-    }
-
-    protected void dispatchOnRequestFailed(int reason) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onRequestFailed(reason);
-        }
-    }
-
-    private Collection<MediaDeviceCallback> getCallbacks() {
-        return new CopyOnWriteArrayList<>(mCallbacks);
-    }
-
-    /**
-     * Callback for notifying device is added, removed and attributes changed.
-     */
-    public interface MediaDeviceCallback {
-
-        /**
-         * Callback for notifying MediaDevice list is added.
-         *
-         * @param devices the MediaDevice list
-         */
-        void onDeviceListAdded(List<MediaDevice> devices);
-
-        /**
-         * Callback for notifying MediaDevice list is removed.
-         *
-         * @param devices the MediaDevice list
-         */
-        void onDeviceListRemoved(List<MediaDevice> devices);
-
-        /**
-         * Callback for notifying connected MediaDevice is changed.
-         *
-         * @param id the id of MediaDevice
-         */
-        void onConnectedDeviceChanged(String id);
-
-        /**
-         * Callback for notifying that transferring is failed.
-         *
-         * @param reason the reason that the request has failed. Can be one of followings:
-         * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
-         * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED},
-         * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR},
-         * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
-         * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
-         */
-        void onRequestFailed(int reason);
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
index ea4de39..ff4d4dd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.media;
 
-import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.RouteListingPreference;
@@ -42,9 +41,8 @@
     NoOpInfoMediaManager(
             Context context,
             @NonNull String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager) {
-        super(context, packageName, notification, localBluetoothManager);
+        super(context, packageName, localBluetoothManager);
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
index df03167..9c82cb1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -17,7 +17,6 @@
 package com.android.settingslib.media;
 
 import android.annotation.SuppressLint;
-import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2;
@@ -71,10 +70,9 @@
     /* package */ RouterInfoMediaManager(
             Context context,
             @NonNull String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager)
             throws PackageNotAvailableException {
-        super(context, packageName, notification, localBluetoothManager);
+        super(context, packageName, localBluetoothManager);
 
         MediaRouter2 router = null;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
index 0117ece..d5444cf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
@@ -53,7 +53,7 @@
          * @param noInternet True if a connected Wi-Fi network cannot access the Internet
          * @param level The number of bars to show (0-4)
          */
-        fun getIcon(noInternet: Boolean, level: Int): Drawable? {
+        open fun getIcon(noInternet: Boolean, level: Int): Drawable? {
             return context.getDrawable(getInternetIconResource(level, noInternet))
         }
     }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index 1ad7d49..fe83ffb 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -319,7 +319,8 @@
 
     @Test
     public void testPrivateProfileFilterDisplaysCorrectApps() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
 
         mEntry.showInPersonalTab = true;
         mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
@@ -334,7 +335,8 @@
 
     @Test
     public void testPrivateProfileFilterDisplaysCorrectAppsWhenFlagDisabled() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.disableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
 
         mEntry.showInPersonalTab = false;
         mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_PRIVATE;
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java
index c647cbb..f0185b9 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java
@@ -64,22 +64,21 @@
     @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOn_returnsRouterInfoMediaManager() {
         InfoMediaManager manager =
-                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null, null);
+                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null);
         assertThat(manager).isInstanceOf(RouterInfoMediaManager.class);
     }
 
     @Test
     @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOn_withFakePackage_returnsNoOpInfoMediaManager() {
-        InfoMediaManager manager =
-                InfoMediaManager.createInstance(mContext, FAKE_PACKAGE, null, null);
+        InfoMediaManager manager = InfoMediaManager.createInstance(mContext, FAKE_PACKAGE, null);
         assertThat(manager).isInstanceOf(NoOpInfoMediaManager.class);
     }
 
     @Test
     @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOn_withNullPackage_returnsRouterInfoMediaManager() {
-        InfoMediaManager manager = InfoMediaManager.createInstance(mContext, null, null, null);
+        InfoMediaManager manager = InfoMediaManager.createInstance(mContext, null, null);
         assertThat(manager).isInstanceOf(RouterInfoMediaManager.class);
     }
 
@@ -87,7 +86,7 @@
     @RequiresFlagsDisabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOff_returnsManagerInfoMediaManager() {
         InfoMediaManager manager =
-                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null, null);
+                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null);
         assertThat(manager).isInstanceOf(ManagerInfoMediaManager.class);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 827d8fa..3b18aa3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -72,6 +72,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -499,6 +500,7 @@
         verify(mApplicationsState, never()).clearEntries();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void removeProfileApp_workprofileExists_doResumeIfNeededLocked_shouldClearEntries()
             throws RemoteException {
@@ -573,6 +575,7 @@
         verify(mApplicationsState).clearEntries();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void removeOwnerApp_workprofileExists_doResumeIfNeededLocked_shouldClearEntries()
             throws RemoteException {
@@ -654,6 +657,7 @@
         verify(mApplicationsState).clearEntries();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void noAppRemoved_workprofileExists_doResumeIfNeededLocked_shouldNotClearEntries()
             throws RemoteException {
@@ -773,6 +777,7 @@
         assertThat(primaryUserApp.shouldShowInPersonalTab(um, appInfo.uid)).isTrue();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void shouldShowInPersonalTab_userProfilePreU_returnsFalse() {
         UserManager um = RuntimeEnvironment.application.getSystemService(UserManager.class);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index c159d5e..d85d253 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -91,7 +91,7 @@
     @Mock
     private LocalBluetoothManager mLocalBluetoothManager;
     @Mock
-    private MediaManager.MediaDeviceCallback mCallback;
+    private InfoMediaManager.MediaDeviceCallback mCallback;
     @Mock
     private MediaSessionManager mMediaSessionManager;
     @Mock
@@ -109,8 +109,7 @@
         doReturn(mMediaSessionManager).when(mContext).getSystemService(
                 Context.MEDIA_SESSION_SERVICE);
         mInfoMediaManager =
-                new ManagerInfoMediaManager(
-                        mContext, TEST_PACKAGE_NAME, null, mLocalBluetoothManager);
+                new ManagerInfoMediaManager(mContext, TEST_PACKAGE_NAME, mLocalBluetoothManager);
         mShadowRouter2Manager = ShadowRouter2Manager.getShadow();
         mInfoMediaManager.mRouterManager = MediaRouter2Manager.getInstance(mContext);
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 9a7d4f1..693b7d0 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -35,7 +36,6 @@
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.MediaRoute2Info;
-import android.media.MediaRouter2Manager;
 import android.media.RoutingSessionInfo;
 
 import com.android.settingslib.bluetooth.A2dpProfile;
@@ -75,8 +75,6 @@
     private static final String TEST_ADDRESS = "00:01:02:03:04:05";
 
     @Mock
-    private InfoMediaManager mInfoMediaManager;
-    @Mock
     private LocalBluetoothManager mLocalBluetoothManager;
     @Mock
     private LocalMediaManager.DeviceCallback mCallback;
@@ -87,8 +85,6 @@
     @Mock
     private LocalBluetoothProfileManager mLocalProfileManager;
     @Mock
-    private MediaRouter2Manager mMediaRouter2Manager;
-    @Mock
     private MediaRoute2Info mRouteInfo1;
     @Mock
     private MediaRoute2Info mRouteInfo2;
@@ -97,6 +93,7 @@
 
     private Context mContext;
     private LocalMediaManager mLocalMediaManager;
+    private InfoMediaManager mInfoMediaManager;
     private ShadowBluetoothAdapter mShadowBluetoothAdapter;
     private InfoMediaDevice mInfoMediaDevice1;
     private InfoMediaDevice mInfoMediaDevice2;
@@ -116,10 +113,16 @@
         when(mLocalProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
         when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHapProfile);
 
+        // Need to call constructor to initialize final fields.
+        mInfoMediaManager = mock(
+                InfoMediaManager.class,
+                withSettings().useConstructor(mContext, TEST_PACKAGE_NAME, mLocalBluetoothManager));
+
         mInfoMediaDevice1 = spy(new InfoMediaDevice(mContext, mRouteInfo1));
         mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2);
-        mLocalMediaManager = new LocalMediaManager(mContext, mLocalBluetoothManager,
-                mInfoMediaManager, "com.test.packagename");
+        mLocalMediaManager =
+                new LocalMediaManager(
+                        mContext, mLocalBluetoothManager, mInfoMediaManager, TEST_PACKAGE_NAME);
         mLocalMediaManager.mAudioManager = mAudioManager;
     }
 
@@ -146,7 +149,6 @@
 
         mLocalMediaManager.registerCallback(mCallback);
         assertThat(mLocalMediaManager.connectDevice(device)).isTrue();
-
         verify(mInfoMediaManager).connectToDevice(device);
     }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaManagerTest.java
deleted file mode 100644
index c3237f0..0000000
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaManagerTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 2019 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.media;
-
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-
-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.RuntimeEnvironment;
-
-import java.util.Collections;
-
-@RunWith(RobolectricTestRunner.class)
-public class MediaManagerTest {
-
-    private static final String TEST_ID = "test_id";
-
-    @Mock
-    private MediaManager.MediaDeviceCallback mCallback;
-    @Mock
-    private MediaDevice mDevice;
-
-    private MediaManager mMediaManager;
-    private Context mContext;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
-
-        when(mDevice.getId()).thenReturn(TEST_ID);
-
-        mMediaManager = new MediaManager(mContext, null) {};
-    }
-
-    @Test
-    public void dispatchDeviceListAdded_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchDeviceListAdded(Collections.emptyList());
-
-        verify(mCallback).onDeviceListAdded(any());
-    }
-
-    @Test
-    public void dispatchDeviceListRemoved_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchDeviceListRemoved(Collections.emptyList());
-
-        verify(mCallback).onDeviceListRemoved(Collections.emptyList());
-    }
-
-    @Test
-    public void dispatchActiveDeviceChanged_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchConnectedDeviceChanged(TEST_ID);
-
-        verify(mCallback).onConnectedDeviceChanged(TEST_ID);
-    }
-
-    @Test
-    public void dispatchOnRequestFailed_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchOnRequestFailed(1);
-
-        verify(mCallback).onRequestFailed(1);
-    }
-
-}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index add3134..8f8445d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -115,6 +115,10 @@
         Settings.Global.Wearable.SCREEN_UNLOCK_SOUND_ENABLED,
         Settings.Global.Wearable.CHARGING_SOUNDS_ENABLED,
         Settings.Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED,
+        Settings.Global.Wearable.AUTO_BEDTIME_MODE,
         Settings.Global.FORCE_ENABLE_PSS_PROFILING,
+        Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED,
+        Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE,
+        Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index c0a0760..6def40b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -452,6 +452,7 @@
         VALIDATORS.put(Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(
                 Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED, ANY_INTEGER_VALIDATOR);
+        VALIDATORS.put(Global.Wearable.AUTO_BEDTIME_MODE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.FORCE_ENABLE_PSS_PROFILING, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index ce0257f..2a8eb9b 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -72,6 +72,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -157,6 +158,9 @@
             "/product/etc/aconfig_flags.pb",
             "/vendor/etc/aconfig_flags.pb");
 
+    private static final String APEX_DIR = "/apex";
+    private static final String APEX_ACONFIG_PATH_SUFFIX = "/etc/aconfig_flags.pb";
+
     /**
      * This tag is applied to all aconfig default value-loaded flags.
      */
@@ -238,7 +242,7 @@
     private int mNextHistoricalOpIdx;
 
     @GuardedBy("mLock")
-    @Nullable
+    @NonNull
     private Map<String, Map<String, String>> mNamespaceDefaults;
 
     public static final int SETTINGS_TYPE_GLOBAL = 0;
@@ -332,23 +336,29 @@
         mHistoricalOperations = Build.IS_DEBUGGABLE
                 ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null;
 
+        mNamespaceDefaults = new HashMap<>();
+
         synchronized (mLock) {
             readStateSyncLocked();
 
             if (Flags.loadAconfigDefaults()) {
                 if (isConfigSettingsKey(mKey)) {
-                    loadAconfigDefaultValuesLocked();
+                    loadAconfigDefaultValuesLocked(sAconfigTextProtoFilesOnDevice);
                 }
             }
 
+            if (Flags.loadApexAconfigProtobufs()) {
+                if (isConfigSettingsKey(mKey)) {
+                    List<String> apexProtoPaths = listApexProtoPaths();
+                    loadAconfigDefaultValuesLocked(apexProtoPaths);
+                }
+            }
         }
     }
 
     @GuardedBy("mLock")
-    private void loadAconfigDefaultValuesLocked() {
-        mNamespaceDefaults = new HashMap<>();
-
-        for (String fileName : sAconfigTextProtoFilesOnDevice) {
+    private void loadAconfigDefaultValuesLocked(List<String> filePaths) {
+        for (String fileName : filePaths) {
             try (FileInputStream inputStream = new FileInputStream(fileName)) {
                 loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults);
             } catch (IOException e) {
@@ -357,13 +367,41 @@
         }
     }
 
+    private List<String> listApexProtoPaths() {
+        LinkedList<String> paths = new LinkedList();
+
+        File apexDirectory = new File(APEX_DIR);
+        if (!apexDirectory.isDirectory()) {
+            return paths;
+        }
+
+        File[] subdirs = apexDirectory.listFiles();
+        if (subdirs == null) {
+            return paths;
+        }
+
+        for (File prefix : subdirs) {
+            // For each mainline modules, there are two directories, one <modulepackage>/,
+            // and one <modulepackage>@<versioncode>/. Just read the former.
+            if (prefix.getAbsolutePath().contains("@")) {
+                continue;
+            }
+
+            File protoPath = new File(prefix + APEX_ACONFIG_PATH_SUFFIX);
+            if (!protoPath.exists()) {
+                continue;
+            }
+
+            paths.add(protoPath.getAbsolutePath());
+        }
+        return paths;
+    }
+
     @VisibleForTesting
     @GuardedBy("mLock")
     public void addAconfigDefaultValuesFromMap(
             @NonNull Map<String, Map<String, String>> defaultMap) {
-        if (mNamespaceDefaults != null) {
-            mNamespaceDefaults.putAll(defaultMap);
-        }
+        mNamespaceDefaults.putAll(defaultMap);
     }
 
     @VisibleForTesting
@@ -447,7 +485,7 @@
         return names;
     }
 
-    @Nullable
+    @NonNull
     public Map<String, Map<String, String>> getAconfigDefaultValues() {
         synchronized (mLock) {
             return mNamespaceDefaults;
@@ -519,9 +557,9 @@
             return false;
         }
 
-        // Aconfig flags are always boot stable, so we anytime we write one, we staged it to be
+        // Aconfig flags are always boot stable, so we anytime we write one, we stage it to be
         // applied on reboot.
-        if (Flags.stageAllAconfigFlags() && mNamespaceDefaults != null) {
+        if (Flags.stageAllAconfigFlags()) {
             int slashIndex = name.indexOf("/");
             boolean stageFlag = isConfigSettingsKey(mKey)
                     && slashIndex != -1
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index e5086e8..2e14e9b 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -25,3 +25,11 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "load_apex_aconfig_protobufs"
+    namespace: "core_experiments_team_internal"
+    description: "When enabled, loads aconfig default values in apex flag protobufs into DeviceConfig on boot."
+    bug: "327383546"
+    is_fixed_read_only: true
+}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 28cdc6d..09d076e 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -626,9 +626,6 @@
                     Settings.Global.Wearable.BEDTIME_MODE,
                     Settings.Global.Wearable.BEDTIME_HARD_MODE,
                     Settings.Global.Wearable.LOCK_SCREEN_STATE,
-                    Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED,
-                    Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE,
-                    Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED,
                     Settings.Global.Wearable.DISABLE_AOD_WHILE_PLUGGED,
                     Settings.Global.Wearable.NETWORK_LOCATION_OPT_IN,
                     Settings.Global.Wearable.CUSTOM_COLOR_FOREGROUND,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index ea30c69..33362a2 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -25,7 +25,6 @@
 import android.aconfig.Aconfig.parsed_flag;
 import android.aconfig.Aconfig.parsed_flags;
 import android.os.Looper;
-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;
@@ -231,38 +230,6 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_LOAD_ACONFIG_DEFAULTS)
-    public void testAddingAconfigMapOnNullIsNoOp() {
-        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
-        Object lock = new Object();
-        SettingsState settingsState = new SettingsState(
-                InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey,
-                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
-
-        parsed_flags flags = parsed_flags
-                .newBuilder()
-                .addParsedFlag(parsed_flag
-                    .newBuilder()
-                        .setPackage("com.android.flags")
-                        .setName("flag5")
-                        .setNamespace("test_namespace")
-                        .setDescription("test flag")
-                        .addBug("12345678")
-                        .setState(Aconfig.flag_state.DISABLED)
-                        .setPermission(Aconfig.flag_permission.READ_WRITE))
-                .build();
-
-        synchronized (lock) {
-            Map<String, Map<String, String>> defaults = new HashMap<>();
-            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
-            settingsState.addAconfigDefaultValuesFromMap(defaults);
-
-            assertEquals(null, settingsState.getAconfigDefaultValues());
-        }
-
-    }
-
-    @Test
     public void testInvalidAconfigProtoDoesNotCrash() {
         Map<String, Map<String, String>> defaults = new HashMap<>();
         SettingsState settingsState = getSettingStateObject();
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 0c02f56..eb2d13d 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -566,9 +566,6 @@
     <!-- Permission required for CTS test - android.server.biometrics -->
     <uses-permission android:name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" />
 
-    <!-- Permission required for CTS test - android.server.biometrics -->
-    <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
-
     <!-- Permissions required for CTS test - NotificationManagerTest -->
     <uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" />
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cc2e84c..04cb88d 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -308,6 +308,7 @@
     name: "SystemUI-tests",
     use_resource_processor: true,
     manifest: "tests/AndroidManifest-base.xml",
+    resource_dirs: [],
     additional_manifests: ["tests/AndroidManifest.xml"],
     srcs: [
         "tests/src/**/*.kt",
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index d0e6b28..7bbe82c 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -4,26 +4,26 @@
     name: "predictive_back_sysui"
     namespace: "systemui"
     description: "Predictive Back Dispatching for SysUI"
-    bug: "309545085"
+    bug: "327737297"
 }
 
 flag {
     name: "predictive_back_animate_shade"
     namespace: "systemui"
     description: "Enable Shade Animations"
-    bug: "309545085"
+    bug: "327732946"
 }
 
 flag {
     name: "predictive_back_animate_bouncer"
     namespace: "systemui"
     description: "Enable Predictive Back Animation in Bouncer"
-    bug: "309545085"
+    bug: "327733487"
 }
 
 flag {
     name: "predictive_back_animate_dialogs"
     namespace: "systemui"
     description: "Enable Predictive Back Animation for SysUI dialogs"
-    bug: "309545085"
+    bug: "327721544"
 }
\ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8c8975f..d05d40d 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -25,6 +25,16 @@
 }
 
 flag {
+   name: "notification_view_flipper_pausing"
+   namespace: "systemui"
+   description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
+   bug: "309146176"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
     name: "notification_async_group_header_inflation"
     namespace: "systemui"
     description: "Inflates the notification group summary header views from the background thread."
@@ -194,14 +204,6 @@
 }
 
 flag {
-    name: "keyguard_shade_migration_nssl"
-    namespace: "systemui"
-    description: "Moves NSSL into a shared element between the notification_panel and "
-        "keyguard_root_view."
-    bug: "278054201"
-}
-
-flag {
     name: "unfold_animation_background_progress"
     namespace: "systemui"
     description: "Moves unfold animation progress calculation to a background thread"
@@ -419,6 +421,13 @@
 }
 
 flag {
+   name: "smartspace_remoteviews_rendering"
+   namespace: "systemui"
+   description: "Indicate Smartspace RemoteViews rendering"
+   bug: "326292691"
+}
+
+flag {
    name: "pin_input_field_styled_focus_state"
    namespace: "systemui"
    description: "Enables styled focus states on pin input field if keyboard is connected"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 8dc7495..b124025 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -27,6 +27,7 @@
 import android.text.Layout
 import android.util.LruCache
 import kotlin.math.roundToInt
+import android.util.Log
 
 private const val DEFAULT_ANIMATION_DURATION: Long = 300
 private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
@@ -140,7 +141,6 @@
     }
 
     sealed class PositionedGlyph {
-
         /** Mutable X coordinate of the glyph position relative from drawing offset. */
         var x: Float = 0f
 
@@ -269,41 +269,53 @@
         duration: Long = -1L,
         interpolator: TimeInterpolator? = null,
         delay: Long = 0,
-        onAnimationEnd: Runnable? = null
+        onAnimationEnd: Runnable? = null,
+    ) = setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration,
+        interpolator, delay, onAnimationEnd, updateLayoutOnFailure = true)
+
+    private fun setTextStyleInternal(
+        fvar: String?,
+        textSize: Float,
+        color: Int?,
+        strokeWidth: Float,
+        animate: Boolean,
+        duration: Long,
+        interpolator: TimeInterpolator?,
+        delay: Long,
+        onAnimationEnd: Runnable?,
+        updateLayoutOnFailure: Boolean,
     ) {
-        if (animate) {
-            animator.cancel()
-            textInterpolator.rebase()
-        }
+        try {
+            if (animate) {
+                animator.cancel()
+                textInterpolator.rebase()
+            }
 
-        if (textSize >= 0) {
-            textInterpolator.targetPaint.textSize = textSize
-        }
+            if (textSize >= 0) {
+                textInterpolator.targetPaint.textSize = textSize
+            }
+            if (!fvar.isNullOrBlank()) {
+                textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
+            }
+            if (color != null) {
+                textInterpolator.targetPaint.color = color
+            }
+            if (strokeWidth >= 0F) {
+                textInterpolator.targetPaint.strokeWidth = strokeWidth
+            }
+            textInterpolator.onTargetPaintModified()
 
-        if (!fvar.isNullOrBlank()) {
-            textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
-        }
-
-        if (color != null) {
-            textInterpolator.targetPaint.color = color
-        }
-        if (strokeWidth >= 0F) {
-            textInterpolator.targetPaint.strokeWidth = strokeWidth
-        }
-        textInterpolator.onTargetPaintModified()
-
-        if (animate) {
-            animator.startDelay = delay
-            animator.duration =
-                if (duration == -1L) {
-                    DEFAULT_ANIMATION_DURATION
-                } else {
-                    duration
-                }
-            interpolator?.let { animator.interpolator = it }
-            if (onAnimationEnd != null) {
-                val listener =
-                    object : AnimatorListenerAdapter() {
+            if (animate) {
+                animator.startDelay = delay
+                animator.duration =
+                    if (duration == -1L) {
+                        DEFAULT_ANIMATION_DURATION
+                    } else {
+                        duration
+                    }
+                interpolator?.let { animator.interpolator = it }
+                if (onAnimationEnd != null) {
+                    val listener = object : AnimatorListenerAdapter() {
                         override fun onAnimationEnd(animation: Animator) {
                             onAnimationEnd.run()
                             animator.removeListener(this)
@@ -312,14 +324,25 @@
                             animator.removeListener(this)
                         }
                     }
-                animator.addListener(listener)
+                    animator.addListener(listener)
+                }
+                animator.start()
+            } else {
+                // No animation is requested, thus set base and target state to the same state.
+                textInterpolator.progress = 1f
+                textInterpolator.rebase()
+                invalidateCallback()
             }
-            animator.start()
-        } else {
-            // No animation is requested, thus set base and target state to the same state.
-            textInterpolator.progress = 1f
-            textInterpolator.rebase()
-            invalidateCallback()
+        } catch (ex: IllegalArgumentException) {
+            if (updateLayoutOnFailure) {
+                Log.e(TAG, "setTextStyleInternal: Exception caught but retrying. This is usually" +
+                    " due to the layout having changed unexpectedly without being notified.", ex)
+                updateLayout(textInterpolator.layout)
+                setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration,
+                    interpolator, delay, onAnimationEnd, updateLayoutOnFailure = false)
+            } else {
+                throw ex
+            }
         }
     }
 
@@ -355,15 +378,13 @@
         interpolator: TimeInterpolator? = null,
         delay: Long = 0,
         onAnimationEnd: Runnable? = null
-    ) {
-        val fvar = fontVariationUtils.updateFontVariation(
-            weight = weight,
-            width = width,
-            opticalSize = opticalSize,
-            roundness = roundness,
-        )
-        setTextStyle(
-            fvar = fvar,
+    ) = setTextStyleInternal(
+            fvar = fontVariationUtils.updateFontVariation(
+                weight = weight,
+                width = width,
+                opticalSize = opticalSize,
+                roundness = roundness,
+            ),
             textSize = textSize,
             color = color,
             strokeWidth = strokeWidth,
@@ -372,6 +393,10 @@
             interpolator = interpolator,
             delay = delay,
             onAnimationEnd = onAnimationEnd,
+            updateLayoutOnFailure = true,
         )
+
+    companion object {
+        private val TAG = TextAnimator::class.simpleName!!
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index bd539a7..2a13d49 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -51,6 +51,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.PlatformIconButton
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
+import com.android.systemui.common.ui.compose.SelectedUserAwareInputConnection
 import com.android.systemui.res.R
 
 /** UI for the input part of a password-requiring version of the bouncer. */
@@ -71,6 +72,7 @@
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
     val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
+    val selectedUserId by viewModel.selectedUserId.collectAsState()
 
     DisposableEffect(Unit) {
         viewModel.onShown()
@@ -87,47 +89,51 @@
     val color = MaterialTheme.colorScheme.onSurfaceVariant
     val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }
 
-    TextField(
-        value = password,
-        onValueChange = viewModel::onPasswordInputChanged,
-        enabled = isInputEnabled,
-        visualTransformation = PasswordVisualTransformation(),
-        singleLine = true,
-        textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
-        keyboardOptions =
-            KeyboardOptions(
-                keyboardType = KeyboardType.Password,
-                imeAction = ImeAction.Done,
-            ),
-        keyboardActions =
-            KeyboardActions(
-                onDone = { viewModel.onAuthenticateKeyPressed() },
-            ),
-        modifier =
-            modifier
-                .focusRequester(focusRequester)
-                .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
-                .drawBehind {
-                    drawLine(
-                        color = color,
-                        start = Offset(x = 0f, y = size.height - lineWidthPx),
-                        end = Offset(size.width, y = size.height - lineWidthPx),
-                        strokeWidth = lineWidthPx,
-                    )
-                }
-                .onInterceptKeyBeforeSoftKeyboard { keyEvent ->
-                    if (keyEvent.key == Key.Back) {
-                        viewModel.onImeDismissed()
-                        true
-                    } else {
-                        false
+    SelectedUserAwareInputConnection(selectedUserId) {
+        TextField(
+            value = password,
+            onValueChange = viewModel::onPasswordInputChanged,
+            enabled = isInputEnabled,
+            visualTransformation = PasswordVisualTransformation(),
+            singleLine = true,
+            textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
+            keyboardOptions =
+                KeyboardOptions(
+                    keyboardType = KeyboardType.Password,
+                    imeAction = ImeAction.Done,
+                ),
+            keyboardActions =
+                KeyboardActions(
+                    onDone = { viewModel.onAuthenticateKeyPressed() },
+                ),
+            modifier =
+                modifier
+                    .focusRequester(focusRequester)
+                    .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
+                    .drawBehind {
+                        drawLine(
+                            color = color,
+                            start = Offset(x = 0f, y = size.height - lineWidthPx),
+                            end = Offset(size.width, y = size.height - lineWidthPx),
+                            strokeWidth = lineWidthPx,
+                        )
                     }
-                },
-        trailingIcon =
-            if (isImeSwitcherButtonVisible) {
-                { ImeSwitcherButton(viewModel, color) }
-            } else null
-    )
+                    .onInterceptKeyBeforeSoftKeyboard { keyEvent ->
+                        if (keyEvent.key == Key.Back) {
+                            viewModel.onImeDismissed()
+                            true
+                        } else {
+                            false
+                        }
+                    },
+            trailingIcon =
+                if (isImeSwitcherButtonVisible) {
+                    { ImeSwitcherButton(viewModel, color) }
+                } else {
+                    null
+                }
+        )
+    }
 }
 
 /** Button for changing the password input method (IME). */
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/SelectedUserAwareInputConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/SelectedUserAwareInputConnection.kt
new file mode 100644
index 0000000..c8e1450
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/SelectedUserAwareInputConnection.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalComposeUiApi::class)
+
+package com.android.systemui.common.ui.compose
+
+import android.annotation.UserIdInt
+import android.os.UserHandle
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.InterceptPlatformTextInput
+import androidx.compose.ui.platform.PlatformTextInputMethodRequest
+
+/**
+ * Wrapper for input connection composables that need to be aware of the selected user to connect to
+ * the correct instance of on-device services like autofill, autocorrect, etc.
+ *
+ * Usage:
+ * ```
+ * @Composable
+ * fun YourFunction(viewModel: YourViewModel) {
+ *     val selectedUserId by viewModel.selectedUserId.collectAsState()
+ *
+ *     SelectedUserAwareInputConnection(selectedUserId) {
+ *         TextField(...)
+ *     }
+ * }
+ * ```
+ */
+@Composable
+fun SelectedUserAwareInputConnection(
+    @UserIdInt selectedUserId: Int,
+    content: @Composable () -> Unit,
+) {
+    InterceptPlatformTextInput(
+        interceptor = { request, nextHandler ->
+            // Create a new request to wrap the incoming one with some custom logic.
+            val modifiedRequest =
+                object : PlatformTextInputMethodRequest {
+                    override fun createInputConnection(outAttributes: EditorInfo): InputConnection {
+                        val inputConnection = request.createInputConnection(outAttributes)
+                        // After the original request finishes initializing the EditorInfo we can
+                        // customize it. If we needed to we could also wrap the InputConnection
+                        // before
+                        // returning it.
+                        updateEditorInfo(outAttributes)
+                        return inputConnection
+                    }
+
+                    fun updateEditorInfo(outAttributes: EditorInfo) {
+                        outAttributes.targetInputMethodUser = UserHandle.of(selectedUserId)
+                    }
+                }
+
+            // Send our wrapping request to the next handler, which could be the system or another
+            // interceptor up the tree.
+            nextHandler.startInputMethod(modifiedRequest)
+        }
+    ) {
+        content()
+    }
+}
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 515c816..078da1c86 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
@@ -31,8 +31,8 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -93,7 +93,6 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
@@ -119,7 +118,6 @@
 import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.res.R
 import kotlinx.coroutines.launch
@@ -199,37 +197,26 @@
                     }
                 },
     ) {
-        Column(Modifier.align(Alignment.TopStart)) {
-            CommunalHubLazyGrid(
-                communalContent = communalContent,
-                viewModel = viewModel,
-                contentPadding = contentPadding,
-                contentOffset = contentOffset,
-                setGridCoordinates = { gridCoordinates = it },
-                updateDragPositionForRemove = { offset ->
-                    isDraggingToRemove =
-                        isPointerWithinCoordinates(
-                            offset = gridCoordinates?.let { it.positionInWindow() + offset },
-                            containerToCheck = removeButtonCoordinates
-                        )
-                    isDraggingToRemove
-                },
-                onOpenWidgetPicker = onOpenWidgetPicker,
-                gridState = gridState,
-                contentListState = contentListState,
-                selectedKey = selectedKey,
-                widgetConfigurator = widgetConfigurator,
-            )
-            // TODO(b/326060686): Remove this once keyguard indication area can persist over hub
-            if (viewModel is CommunalViewModel) {
-                val isUnlocked by viewModel.deviceUnlocked.collectAsState(initial = false)
-                Spacer(Modifier.height(24.dp))
-                LockStateIcon(
-                    isUnlocked = isUnlocked,
-                    modifier = Modifier.align(Alignment.CenterHorizontally),
-                )
-            }
-        }
+        CommunalHubLazyGrid(
+            communalContent = communalContent,
+            viewModel = viewModel,
+            contentPadding = contentPadding,
+            contentOffset = contentOffset,
+            setGridCoordinates = { gridCoordinates = it },
+            updateDragPositionForRemove = { offset ->
+                isDraggingToRemove =
+                    isPointerWithinCoordinates(
+                        offset = gridCoordinates?.let { it.positionInWindow() + offset },
+                        containerToCheck = removeButtonCoordinates
+                    )
+                isDraggingToRemove
+            },
+            onOpenWidgetPicker = onOpenWidgetPicker,
+            gridState = gridState,
+            contentListState = contentListState,
+            selectedKey = selectedKey,
+            widgetConfigurator = widgetConfigurator,
+        )
 
         if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
             Toolbar(
@@ -281,7 +268,7 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun ColumnScope.CommunalHubLazyGrid(
+private fun BoxScope.CommunalHubLazyGrid(
     communalContent: List<CommunalContentModel>,
     viewModel: BaseCommunalViewModel,
     contentPadding: PaddingValues,
@@ -295,7 +282,7 @@
     widgetConfigurator: WidgetConfigurator?,
 ) {
     var gridModifier =
-        Modifier.align(Alignment.Start).onGloballyPositioned { setGridCoordinates(it) }
+        Modifier.align(Alignment.TopStart).onGloballyPositioned { setGridCoordinates(it) }
     var list = communalContent
     var dragDropState: GridDragDropState? = null
     if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
@@ -377,26 +364,6 @@
     }
 }
 
-@Composable
-private fun LockStateIcon(
-    isUnlocked: Boolean,
-    modifier: Modifier = Modifier,
-) {
-    val colors = LocalAndroidColorScheme.current
-    val resource =
-        if (isUnlocked) {
-            R.drawable.ic_unlocked
-        } else {
-            R.drawable.ic_lock
-        }
-    Icon(
-        painter = painterResource(id = resource),
-        contentDescription = null,
-        tint = colors.onPrimaryContainer,
-        modifier = modifier.size(52.dp)
-    )
-}
-
 /**
  * Toolbar that contains action buttons to
  * 1) open the widget picker
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
index b3fcc30..53de5bc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
@@ -27,12 +27,14 @@
 import androidx.compose.animation.slideInVertically
 import androidx.compose.animation.slideOutVertically
 import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
@@ -137,12 +139,14 @@
                     }
                 }
             ) { targetViewModel ->
-                Expandable(
-                    modifier = Modifier.fillMaxSize(),
-                    color = targetViewModel.backgroundColor.toColor(),
-                    shape = RoundedCornerShape(12.dp),
-                    onClick = { viewModel.onDeviceClick(it) },
-                ) {}
+                Spacer(
+                    modifier =
+                        Modifier.fillMaxSize()
+                            .background(
+                                color = targetViewModel.backgroundColor.toColor(),
+                                shape = RoundedCornerShape(12.dp),
+                            ),
+                )
             }
             transition.AnimatedContent(
                 contentKey = { it.icon },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 92eb8f8..4950b96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -45,32 +45,32 @@
 import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -102,6 +102,7 @@
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
 class UdfpsControllerOverlayTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
 
     @JvmField @Rule var rule = MockitoJUnit.rule()
 
@@ -148,28 +149,11 @@
 
     @Before
     fun setup() {
-        testScope = TestScope(StandardTestDispatcher())
-        powerRepository = FakePowerRepository()
-        powerInteractor =
-            PowerInteractor(
-                powerRepository,
-                mock(FalsingCollector::class.java),
-                mock(ScreenOffAnimationController::class.java),
-                statusBarStateController,
-            )
-        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-        keyguardTransitionInteractor =
-            KeyguardTransitionInteractor(
-                scope = testScope.backgroundScope,
-                repository = keyguardTransitionRepository,
-                fromLockscreenTransitionInteractor = {
-                    mock(FromLockscreenTransitionInteractor::class.java)
-                },
-                fromPrimaryBouncerTransitionInteractor = {
-                    mock(FromPrimaryBouncerTransitionInteractor::class.java)
-                },
-                fromAodTransitionInteractor = { mock(FromAodTransitionInteractor::class.java) },
-            )
+        testScope = kosmos.testScope
+        powerRepository = kosmos.fakePowerRepository
+        powerInteractor = kosmos.powerInteractor
+        keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+        keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
         whenever(inflater.inflate(R.layout.udfps_view, null, false)).thenReturn(udfpsView)
         whenever(inflater.inflate(R.layout.udfps_bp_view, null))
             .thenReturn(mock(UdfpsBpView::class.java))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 8f802b8..563aad1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -114,7 +113,6 @@
                 kosmos.communalInteractor,
                 kosmos.communalTutorialInteractor,
                 kosmos.shadeInteractor,
-                kosmos.deviceEntryInteractor,
                 mediaHost,
                 logcatLogBuffer("CommunalViewModelTest"),
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 19b80da..128b465 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.common.shared.model.Position
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.doze.DozeMachine
 import com.android.systemui.doze.DozeTransitionCallback
@@ -151,6 +152,24 @@
         }
 
     @Test
+    fun clockPosition() =
+        testScope.runTest {
+            assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0))
+
+            underTest.setClockPosition(0, 1)
+            assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 1))
+
+            underTest.setClockPosition(1, 9)
+            assertThat(underTest.clockPosition.value).isEqualTo(Position(1, 9))
+
+            underTest.setClockPosition(1, 0)
+            assertThat(underTest.clockPosition.value).isEqualTo(Position(1, 0))
+
+            underTest.setClockPosition(3, 1)
+            assertThat(underTest.clockPosition.value).isEqualTo(Position(3, 1))
+        }
+
+    @Test
     fun dozeTimeTick() =
         testScope.runTest {
             val lastDozeTimeTick by collectLastValue(underTest.dozeTimeTick)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index fb46ed9d..b3380ff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -25,18 +25,17 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -49,9 +48,10 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class LightRevealScrimRepositoryTest : SysuiTestCase() {
-    private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
-    private lateinit var powerRepository: FakePowerRepository
-    private lateinit var powerInteractor: PowerInteractor
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
+    private val powerInteractor = kosmos.powerInteractor
     private lateinit var underTest: LightRevealScrimRepositoryImpl
 
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
@@ -59,13 +59,13 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        fakeKeyguardRepository = FakeKeyguardRepository()
-        powerRepository = FakePowerRepository()
-        powerInteractor =
-            PowerInteractorFactory.create(repository = powerRepository).powerInteractor
-
         underTest =
-            LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context, powerInteractor, mock())
+            LightRevealScrimRepositoryImpl(
+                kosmos.fakeKeyguardRepository,
+                context,
+                kosmos.powerInteractor,
+                mock()
+            )
     }
 
     @Test
@@ -168,8 +168,9 @@
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     fun revealAmount_emitsTo1AfterAnimationStarted() =
-        runTest(UnconfinedTestDispatcher()) {
+        testScope.runTest {
             val value by collectLastValue(underTest.revealAmount)
+            runCurrent()
             underTest.startRevealAmountAnimator(true)
             assertEquals(0.0f, value)
             animatorTestRule.advanceTimeBy(500L)
@@ -179,8 +180,9 @@
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     fun revealAmount_startingRevealTwiceWontRerunAnimator() =
-        runTest(UnconfinedTestDispatcher()) {
+        testScope.runTest {
             val value by collectLastValue(underTest.revealAmount)
+            runCurrent()
             underTest.startRevealAmountAnimator(true)
             assertEquals(0.0f, value)
             animatorTestRule.advanceTimeBy(250L)
@@ -193,12 +195,14 @@
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
-        runTest(UnconfinedTestDispatcher()) {
-            val value by collectLastValue(underTest.revealAmount)
-            underTest.startRevealAmountAnimator(false)
-            assertEquals(1.0f, value)
+        testScope.runTest {
+            val lastValue by collectLastValue(underTest.revealAmount)
+            runCurrent()
+            underTest.startRevealAmountAnimator(true)
             animatorTestRule.advanceTimeBy(500L)
-            assertEquals(0.0f, value)
+            underTest.startRevealAmountAnimator(false)
+            animatorTestRule.advanceTimeBy(500L)
+            assertEquals(0.0f, lastValue)
         }
 
     /**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index f9ec3d1..24c651f3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -18,9 +18,11 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.app.StatusBarManager
+import android.platform.test.annotations.DisableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
@@ -120,6 +122,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testKeyguardGuardVisibilityStopsSecureCamera() =
         testScope.runTest {
             val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
@@ -144,6 +147,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testBouncerShowingResetsSecureCameraState() =
         testScope.runTest {
             val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
@@ -166,6 +170,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun keyguardVisibilityIsDefinedAsKeyguardShowingButNotOccluded() = runTest {
         val isVisible = collectLastValue(underTest.isKeyguardVisible)
         repository.setKeyguardShowing(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 63abc8f..0ebcf56 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dock.DockManagerFake
 import com.android.systemui.flags.FakeFeatureFlags
@@ -49,6 +50,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
@@ -57,6 +59,7 @@
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -80,6 +83,7 @@
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var launchAnimator: DialogTransitionAnimator
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var shadeInteractor: ShadeInteractor
     @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
 
     private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -179,6 +183,7 @@
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor = withDeps.keyguardInteractor,
+                shadeInteractor = shadeInteractor,
                 lockPatternUtils = lockPatternUtils,
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
@@ -193,6 +198,8 @@
                 backgroundDispatcher = testDispatcher,
                 appContext = context,
             )
+
+        whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f))
     }
 
     @Test
@@ -339,6 +346,25 @@
         }
 
     @Test
+    fun quickAffordance_updateOncePerShadeExpansion() =
+        testScope.runTest {
+            val shadeExpansion = MutableStateFlow(0f)
+            whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion)
+
+            val collectedValue by
+                collectValues(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
+
+            val initialSize = collectedValue.size
+            for (i in 0..10) {
+                shadeExpansion.value = i / 10f
+            }
+
+            assertThat(collectedValue.size).isEqualTo(initialSize + 1)
+        }
+
+    @Test
     fun quickAffordanceAlwaysVisible_notVisible_restrictedByPolicyManager() =
         testScope.runTest {
             whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 225b5b1..b0f59fe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -71,7 +71,7 @@
         mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 
         MockitoAnnotations.initMocks(this)
-        whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
+        whenever(burnInInteractor.keyguardBurnIn).thenReturn(burnInFlow)
         kosmos.burnInInteractor = burnInInteractor
         whenever(goneToAodTransitionViewModel.enterFromTopTranslationY(anyInt()))
             .thenReturn(emptyFlow())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
deleted file mode 100644
index ce089b1..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ /dev/null
@@ -1,186 +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.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags as AConfigFlags
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.shared.model.BurnInModel
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-
-    @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
-    @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
-
-    @Mock private lateinit var burnInInteractor: BurnInInteractor
-    private val burnInFlow = MutableStateFlow(BurnInModel())
-
-    private lateinit var bottomAreaInteractor: KeyguardBottomAreaInteractor
-    private lateinit var underTest: KeyguardIndicationAreaViewModel
-    private lateinit var repository: FakeKeyguardRepository
-
-    private val startButtonFlow =
-        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
-            KeyguardQuickAffordanceViewModel(
-                slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId()
-            )
-        )
-    private val endButtonFlow =
-        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
-            KeyguardQuickAffordanceViewModel(
-                slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId()
-            )
-        )
-    private val alphaFlow = MutableStateFlow<Float>(1f)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
-        whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
-            .thenReturn(RETURNED_BURN_IN_OFFSET)
-        whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
-
-        val withDeps = KeyguardInteractorFactory.create()
-        val keyguardInteractor = withDeps.keyguardInteractor
-        repository = withDeps.repository
-
-        val bottomAreaViewModel: KeyguardBottomAreaViewModel = mock()
-        whenever(bottomAreaViewModel.startButton).thenReturn(startButtonFlow)
-        whenever(bottomAreaViewModel.endButton).thenReturn(endButtonFlow)
-        whenever(bottomAreaViewModel.alpha).thenReturn(alphaFlow)
-        bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository)
-        underTest =
-            KeyguardIndicationAreaViewModel(
-                keyguardInteractor = keyguardInteractor,
-                bottomAreaInteractor = bottomAreaInteractor,
-                keyguardBottomAreaViewModel = bottomAreaViewModel,
-                burnInHelperWrapper = burnInHelperWrapper,
-                burnInInteractor = burnInInteractor,
-                shortcutsCombinedViewModel = shortcutsCombinedViewModel,
-                configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
-            )
-    }
-
-    @Test
-    fun alpha() =
-        testScope.runTest {
-            val value = collectLastValue(underTest.alpha)
-
-            assertThat(value()).isEqualTo(1f)
-            alphaFlow.value = 0.1f
-            assertThat(value()).isEqualTo(0.1f)
-            alphaFlow.value = 0.5f
-            assertThat(value()).isEqualTo(0.5f)
-            alphaFlow.value = 0.2f
-            assertThat(value()).isEqualTo(0.2f)
-            alphaFlow.value = 0f
-            assertThat(value()).isEqualTo(0f)
-        }
-
-    @Test
-    fun isIndicationAreaPadded() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val value = collectLastValue(underTest.isIndicationAreaPadded)
-
-            assertThat(value()).isFalse()
-            startButtonFlow.value = startButtonFlow.value.copy(isVisible = true)
-            assertThat(value()).isTrue()
-            endButtonFlow.value = endButtonFlow.value.copy(isVisible = true)
-            assertThat(value()).isTrue()
-            startButtonFlow.value = startButtonFlow.value.copy(isVisible = false)
-            assertThat(value()).isTrue()
-            endButtonFlow.value = endButtonFlow.value.copy(isVisible = false)
-            assertThat(value()).isFalse()
-        }
-
-    @Test
-    fun indicationAreaTranslationX() =
-        testScope.runTest {
-            val value = collectLastValue(underTest.indicationAreaTranslationX)
-
-            assertThat(value()).isEqualTo(0f)
-            bottomAreaInteractor.setClockPosition(100, 100)
-            assertThat(value()).isEqualTo(100f)
-            bottomAreaInteractor.setClockPosition(200, 100)
-            assertThat(value()).isEqualTo(200f)
-            bottomAreaInteractor.setClockPosition(200, 200)
-            assertThat(value()).isEqualTo(200f)
-            bottomAreaInteractor.setClockPosition(300, 100)
-            assertThat(value()).isEqualTo(300f)
-        }
-
-    @Test
-    fun indicationAreaTranslationY() =
-        testScope.runTest {
-            val value =
-                collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
-
-            // Negative 0 - apparently there's a difference in floating point arithmetic - FML
-            assertThat(value()).isEqualTo(-0f)
-            val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
-            assertThat(value()).isEqualTo(expected1)
-            val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
-            assertThat(value()).isEqualTo(expected2)
-            val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
-            assertThat(value()).isEqualTo(expected3)
-            val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
-            assertThat(value()).isEqualTo(expected4)
-        }
-
-    private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
-        repository.setDozeAmount(dozeAmount)
-        return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
-    }
-
-    companion object {
-        private const val DEFAULT_BURN_IN_OFFSET = 5
-        private const val RETURNED_BURN_IN_OFFSET = 3
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileDataInteractorTest.kt
new file mode 100644
index 0000000..c80eb13
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileDataInteractorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.doman.interactor
+
+import android.os.UserHandle
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.testing.LeakCheck
+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.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileDataInteractor
+import com.android.systemui.utils.leaks.FakeBatteryController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class BatterySaverTileDataInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private val batteryController = FakeBatteryController(LeakCheck())
+    private val testUser = UserHandle.of(1)
+    private val underTest =
+        BatterySaverTileDataInteractor(testScope.testScheduler, batteryController)
+
+    @Test
+    fun availability_isTrue() =
+        testScope.runTest {
+            val availability = underTest.availability(testUser).toCollection(mutableListOf())
+
+            Truth.assertThat(availability).hasSize(1)
+            Truth.assertThat(availability.last()).isTrue()
+        }
+
+    @Test
+    fun tileData_matchesBatteryControllerPowerSaving() =
+        testScope.runTest {
+            val data by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            runCurrent()
+            Truth.assertThat(data!!.isPowerSaving).isFalse()
+
+            batteryController.setPowerSaveMode(true)
+            runCurrent()
+            Truth.assertThat(data!!.isPowerSaving).isTrue()
+
+            batteryController.setPowerSaveMode(false)
+            runCurrent()
+            Truth.assertThat(data!!.isPowerSaving).isFalse()
+        }
+
+    @Test
+    fun tileData_matchesBatteryControllerIsPluggedIn() =
+        testScope.runTest {
+            val data by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            runCurrent()
+            Truth.assertThat(data!!.isPluggedIn).isFalse()
+
+            batteryController.isPluggedIn = true
+            runCurrent()
+            Truth.assertThat(data!!.isPluggedIn).isTrue()
+
+            batteryController.isPluggedIn = false
+            runCurrent()
+            Truth.assertThat(data!!.isPluggedIn).isFalse()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..62c51e6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileUserActionInteractorTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.doman.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.utils.leaks.FakeBatteryController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class BatterySaverTileUserActionInteractorTest : SysuiTestCase() {
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+    private val controller = FakeBatteryController(LeakCheck())
+    private val underTest = BatterySaverTileUserActionInteractor(inputHandler, controller)
+
+    @Test
+    fun handleClickWhenNotPluggedIn_flipsPowerSaverMode() = runTest {
+        val originalPowerSaveMode = controller.isPowerSave
+        controller.isPluggedIn = false
+
+        underTest.handleInput(
+            QSTileInputTestKtx.click(BatterySaverTileModel.Standard(false, originalPowerSaveMode))
+        )
+
+        Truth.assertThat(controller.isPowerSave).isNotEqualTo(originalPowerSaveMode)
+    }
+
+    @Test
+    fun handleClickWhenPluggedIn_doesNotTurnOnPowerSaverMode() = runTest {
+        controller.setPowerSaveMode(false)
+        val originalPowerSaveMode = controller.isPowerSave
+        controller.isPluggedIn = true
+
+        underTest.handleInput(
+            QSTileInputTestKtx.click(
+                BatterySaverTileModel.Standard(controller.isPluggedIn, originalPowerSaveMode)
+            )
+        )
+
+        Truth.assertThat(controller.isPowerSave).isEqualTo(originalPowerSaveMode)
+    }
+
+    @Test
+    fun handleLongClick() = runTest {
+        underTest.handleInput(
+            QSTileInputTestKtx.longClick(BatterySaverTileModel.Standard(false, false))
+        )
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_BATTERY_SAVER_SETTINGS)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
new file mode 100644
index 0000000..6e9db2c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BatterySaverTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val batterySaverTileConfig = kosmos.qsBatterySaverTileConfig
+    private lateinit var mapper: BatterySaverTileMapper
+
+    @Before
+    fun setup() {
+        mapper =
+            BatterySaverTileMapper(
+                context.orCreateTestableResources
+                    .apply {
+                        addOverride(R.drawable.qs_battery_saver_icon_off, TestStubDrawable())
+                        addOverride(R.drawable.qs_battery_saver_icon_on, TestStubDrawable())
+                    }
+                    .resources,
+                context.theme,
+            )
+    }
+
+    @Test
+    fun map_standard_notPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(false, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.INACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_standard_notPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(false, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.ACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_standard_pluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(true, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_standard_pluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(true, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledNotPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, false, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.INACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledNotPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, true, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.standard_battery_saver_text),
+                R.drawable.qs_battery_saver_icon_on,
+                context.getString(R.string.standard_battery_saver_text),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, true, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, false, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledNotPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, false, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.INACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledNotPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, true, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.extreme_battery_saver_text),
+                R.drawable.qs_battery_saver_icon_on,
+                context.getString(R.string.extreme_battery_saver_text),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, true, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, false, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createBatterySaverTileState(
+        activationState: QSTileState.ActivationState,
+        secondaryLabel: String,
+        iconRes: Int,
+        stateDescription: CharSequence?,
+    ): QSTileState {
+        val label = context.getString(R.string.battery_detail_switch_title)
+        return QSTileState(
+            { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            label,
+            activationState,
+            secondaryLabel,
+            if (activationState == QSTileState.ActivationState.UNAVAILABLE)
+                setOf(QSTileState.UserAction.LONG_CLICK)
+            else setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            label,
+            stateDescription,
+            QSTileState.SideViewIcon.None,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
new file mode 100644
index 0000000..39755bf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InternetTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val internetTileConfig = kosmos.qsInternetTileConfig
+    private val mapper by lazy {
+        InternetTileMapper(
+            context.orCreateTestableResources
+                .apply {
+                    addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable())
+                    addOverride(wifiRes, TestStubDrawable())
+                }
+                .resources,
+            context.theme,
+            context
+        )
+    }
+
+    @Test
+    fun withActiveModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                iconId = wifiRes,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_internet_label),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.quick_settings_networks_available),
+                Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
+                context.getString(R.string.quick_settings_internet_label)
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withInactiveModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Inactive(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.getString(R.string.quick_settings_networks_unavailable),
+                Icon.Loaded(
+                    context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
+                    contentDescription = null
+                ),
+                context.getString(R.string.quick_settings_networks_unavailable)
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createInternetTileState(
+        activationState: QSTileState.ActivationState,
+        secondaryLabel: String,
+        icon: Icon,
+        contentDescription: String,
+    ): QSTileState {
+        val label = context.getString(R.string.quick_settings_internet_label)
+        return QSTileState(
+            { icon },
+            label,
+            activationState,
+            secondaryLabel,
+            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            contentDescription,
+            null,
+            QSTileState.SideViewIcon.Chevron,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+    }
+
+    private companion object {
+        val wifiRes = WIFI_FULL_ICONS[4]
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
new file mode 100644
index 0000000..37ef6ad
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.graphics.drawable.TestStubDrawable
+import android.os.UserHandle
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.AccessibilityContentDescriptions
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@TestableLooper.RunWithLooper
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InternetTileDataInteractorTest : SysuiTestCase() {
+    private val testUser = UserHandle.of(1)
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: InternetTileDataInteractor
+    private lateinit var mobileIconsInteractor: MobileIconsInteractor
+
+    private val airplaneModeRepository = FakeAirplaneModeRepository()
+    private val connectivityRepository = FakeConnectivityRepository()
+    private val ethernetInteractor = EthernetInteractor(connectivityRepository)
+    private val wifiRepository = FakeWifiRepository()
+    private val userSetupRepo = FakeUserSetupRepository()
+    private val wifiInteractor =
+        WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)
+
+    private val tableLogBuffer: TableLogBuffer = mock()
+    private val carrierConfigTracker: CarrierConfigTracker = mock()
+
+    private val mobileConnectionsRepository =
+        FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer)
+    private val mobileConnectionRepository =
+        FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer)
+
+    private val flags =
+        FakeFeatureFlagsClassic().also {
+            it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+        }
+
+    private val internet = context.getString(R.string.quick_settings_internet_label)
+
+    @Before
+    fun setUp() {
+        mobileConnectionRepository.apply {
+            setNetworkTypeKey(mobileConnectionsRepository.GSM_KEY)
+            isInService.value = true
+            dataConnectionState.value = DataConnectionState.Connected
+            dataEnabled.value = true
+        }
+
+        mobileConnectionsRepository.apply {
+            activeMobileDataRepository.value = mobileConnectionRepository
+            activeMobileDataSubscriptionId.value = SUB_1_ID
+            setMobileConnectionRepositoryMap(mapOf(SUB_1_ID to mobileConnectionRepository))
+        }
+
+        mobileIconsInteractor =
+            MobileIconsInteractorImpl(
+                mobileConnectionsRepository,
+                carrierConfigTracker,
+                tableLogBuffer,
+                connectivityRepository,
+                userSetupRepo,
+                testScope.backgroundScope,
+                context,
+                flags,
+            )
+
+        context.orCreateTestableResources.apply {
+            addOverride(com.android.internal.R.drawable.ic_signal_cellular, TestStubDrawable())
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_0,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_1,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_2,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_3,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_4,
+                TestStubDrawable()
+            )
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_phone, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_laptop, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_tablet, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_watch, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_auto, TestStubDrawable())
+        }
+
+        underTest =
+            InternetTileDataInteractor(
+                context,
+                testScope.backgroundScope,
+                airplaneModeRepository,
+                connectivityRepository,
+                ethernetInteractor,
+                mobileIconsInteractor,
+                wifiInteractor,
+            )
+    }
+
+    @Test
+    fun noDefault_noNetworksAvailable() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            connectivityRepository.defaultConnections.value = DefaultConnectionModel()
+
+            assertThat(latest?.secondaryLabel)
+                .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable))
+            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable)
+        }
+
+    @Test
+    fun noDefault_networksAvailable() =
+        testScope.runTest {
+            // TODO(b/328419203): support [WifiInteractor.areNetworksAvailable]
+        }
+
+    @Test
+    fun wifiDefaultAndActive() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            val networkModel =
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 4,
+                    ssid = "test ssid",
+                )
+            val wifiIcon =
+                WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true)
+                    as WifiIcon.Visible
+
+            connectivityRepository.setWifiConnected()
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+
+            assertThat(latest?.secondaryTitle).isEqualTo("test ssid")
+            assertThat(latest?.secondaryLabel).isNull()
+            val expectedIcon =
+                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+
+            val actualIcon = latest?.icon
+            assertThat(actualIcon).isEqualTo(expectedIcon)
+            assertThat(latest?.iconId).isNull()
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo("$internet,test ssid")
+            val expectedSd = wifiIcon.contentDescription
+            assertThat(latest?.stateDescription).isEqualTo(expectedSd)
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotNone() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            val networkModel =
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 4,
+                    ssid = "test ssid",
+                    hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
+                )
+
+            connectivityRepository.setWifiConnected()
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+
+            val expectedIcon =
+                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .doesNotContain(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotTablet() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotLaptop() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotWatch() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotAuto() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotPhone() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotUnknown() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotInvalid() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndNotActive_noNetworksAvailable() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            val networkModel = WifiNetworkModel.Inactive
+
+            connectivityRepository.setWifiConnected(validated = false)
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+            wifiRepository.wifiScanResults.value = emptyList()
+
+            assertThat(latest).isEqualTo(NOT_CONNECTED_NETWORKS_UNAVAILABLE)
+        }
+
+    @Test
+    fun wifiDefaultAndNotActive_networksAvailable() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            val networkModel = WifiNetworkModel.Inactive
+
+            connectivityRepository.setWifiConnected(validated = false)
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+            wifiRepository.wifiScanResults.value = listOf(WifiScanEntry("test 1"))
+
+            assertThat(latest?.secondaryLabel).isNull()
+            assertThat(latest?.secondaryTitle)
+                .isEqualTo(context.getString(R.string.quick_settings_networks_available))
+            assertThat(latest?.icon).isNull()
+            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available)
+            assertThat(latest?.stateDescription).isNull()
+            val expectedCd =
+                "$internet,${context.getString(R.string.quick_settings_networks_available)}"
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(expectedCd)
+        }
+
+    @Test
+    fun mobileDefault_usesNetworkNameAndIcon() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            connectivityRepository.setMobileConnected()
+            mobileConnectionsRepository.mobileIsDefault.value = true
+            mobileConnectionRepository.apply {
+                setAllLevels(3)
+                setAllRoaming(false)
+                networkName.value = NetworkNameModel.Default("test network")
+            }
+
+            assertThat(latest).isNotNull()
+            assertThat(latest?.secondaryTitle).isNotNull()
+            assertThat(latest?.secondaryTitle.toString()).contains("test network")
+            assertThat(latest?.secondaryLabel).isNull()
+            assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java)
+            assertThat(latest?.iconId).isNull()
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(latest?.secondaryTitle.toString())
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(internet)
+        }
+
+    @Test
+    fun ethernetDefault_validated_matchesInteractor() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            val ethernetIcon by collectLastValue(ethernetInteractor.icon)
+
+            connectivityRepository.setEthernetConnected(default = true, validated = true)
+
+            assertThat(latest?.secondaryLabel.loadText(context))
+                .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
+            assertThat(latest?.secondaryTitle).isNull()
+            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully)
+            assertThat(latest?.icon).isNull()
+            assertThat(latest?.stateDescription).isNull()
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(latest?.secondaryLabel.loadText(context))
+        }
+
+    @Test
+    fun ethernetDefault_notValidated_matchesInteractor() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            val ethernetIcon by collectLastValue(ethernetInteractor.icon)
+
+            connectivityRepository.setEthernetConnected(default = true, validated = false)
+
+            assertThat(latest?.secondaryLabel.loadText(context))
+                .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
+            assertThat(latest?.secondaryTitle).isNull()
+            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet)
+            assertThat(latest?.icon).isNull()
+            assertThat(latest?.stateDescription).isNull()
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(latest?.secondaryLabel.loadText(context))
+        }
+
+    private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
+        val networkModel =
+            WifiNetworkModel.Active(
+                networkId = 1,
+                level = 4,
+                ssid = "test ssid",
+                hotspotDeviceType = hotspot,
+            )
+
+        connectivityRepository.setWifiConnected()
+        wifiRepository.setIsWifiDefault(true)
+        wifiRepository.setWifiNetwork(networkModel)
+    }
+
+    private companion object {
+        const val SUB_1_ID = 1
+
+        val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
+            InternetTileModel.Inactive(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..e1f3d97
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.statusbar.connectivity.AccessPointController
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class InternetTileUserActionInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+
+    private lateinit var underTest: InternetTileUserActionInteractor
+
+    @Mock private lateinit var internetDialogManager: InternetDialogManager
+    @Mock private lateinit var controller: AccessPointController
+
+    @Before
+    fun setup() {
+        internetDialogManager = mock<InternetDialogManager>()
+        controller = mock<AccessPointController>()
+
+        underTest =
+            InternetTileUserActionInteractor(
+                kosmos.testScope.coroutineContext,
+                internetDialogManager,
+                controller,
+                inputHandler,
+            )
+    }
+
+    @Test
+    fun handleClickWhenActive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Active()
+
+            underTest.handleInput(QSTileInputTestKtx.click(input))
+
+            verify(internetDialogManager).create(eq(true), anyBoolean(), anyBoolean(), nullable())
+        }
+
+    @Test
+    fun handleClickWhenInactive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Inactive()
+
+            underTest.handleInput(QSTileInputTestKtx.click(input))
+
+            verify(internetDialogManager).create(eq(true), anyBoolean(), anyBoolean(), nullable())
+        }
+
+    @Test
+    fun handleLongClickWhenActive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Active()
+
+            underTest.handleInput(QSTileInputTestKtx.longClick(input))
+
+            QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+                Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
+            }
+        }
+
+    @Test
+    fun handleLongClickWhenInactive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Inactive()
+
+            underTest.handleInput(QSTileInputTestKtx.longClick(input))
+
+            QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+                Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
+            }
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index d505b27..7fabe33 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -25,6 +25,7 @@
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS;
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS;
 import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS;
+import static android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
 import static android.os.UserHandle.USER_ALL;
 import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
@@ -115,7 +116,8 @@
     public static List<FlagsParameterization> getParams() {
         return FlagsParameterization.allCombinationsOf(
                 FLAG_ALLOW_PRIVATE_PROFILE,
-                FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS);
+                FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS,
+                FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
     }
 
     public NotificationLockscreenUserManagerTest(FlagsParameterization flags) {
@@ -872,7 +874,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_ALLOW_PRIVATE_PROFILE)
+    @EnableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
     public void testProfileAvailabilityIntent() {
         mLockscreenUserManager.mCurrentProfiles.clear();
         assertEquals(0, mLockscreenUserManager.mCurrentProfiles.size());
@@ -883,7 +885,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_ALLOW_PRIVATE_PROFILE)
+    @EnableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
     public void testProfileUnAvailabilityIntent() {
         mLockscreenUserManager.mCurrentProfiles.clear();
         assertEquals(0, mLockscreenUserManager.mCurrentProfiles.size());
@@ -894,7 +896,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_ALLOW_PRIVATE_PROFILE)
+    @DisableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
     public void testManagedProfileAvailabilityIntent() {
         mLockscreenUserManager.mCurrentProfiles.clear();
         mLockscreenUserManager.mCurrentManagedProfiles.clear();
@@ -908,7 +910,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_ALLOW_PRIVATE_PROFILE)
+    @DisableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
     public void testManagedProfileUnAvailabilityIntent() {
         mLockscreenUserManager.mCurrentProfiles.clear();
         mLockscreenUserManager.mCurrentManagedProfiles.clear();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
index 243aab2..dcf635e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
@@ -32,8 +32,8 @@
 import com.android.systemui.volume.localMediaRepository
 import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputActionsInteractor
 import com.android.systemui.volume.mediaOutputInteractor
-import com.android.systemui.volume.panel.mediaOutputActionsInteractor
 import com.android.systemui.volume.panel.volumePanelViewModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
index 61f69c0..f6042e4 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
@@ -128,7 +128,8 @@
         android:layout_marginStart="49dp"
         android:layout_marginEnd="49dp"
         android:overScrollMode="never"
-        android:layout_marginBottom="16dp">
+        android:layout_marginBottom="16dp"
+        android:scrollbars="none">
         <LinearLayout
             android:id="@+id/keyboard_shortcuts_container"
             android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/scene_window_root.xml b/packages/SystemUI/res/layout/scene_window_root.xml
index bb8de4c..0dcd15b 100644
--- a/packages/SystemUI/res/layout/scene_window_root.xml
+++ b/packages/SystemUI/res/layout/scene_window_root.xml
@@ -24,7 +24,7 @@
     android:id="@+id/scene_window_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="false">
+    android:fitsSystemWindows="true">
 
     <include layout="@layout/super_notification_shade"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e181d07..35f6a08 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -345,9 +345,6 @@
          the notification is not swiped enough to dismiss it. -->
     <bool name="config_showNotificationGear">true</bool>
 
-    <!-- Whether or not a background should be drawn behind a notification. -->
-    <bool name="config_drawNotificationBackground">false</bool>
-
     <!-- Whether or the notifications can be shown and dismissed with a drag. -->
     <bool name="config_enableNotificationShadeDrag">true</bool>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 035cfdc..71ae0d7 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -223,7 +223,6 @@
     <item type="id" name="lock_icon" />
     <item type="id" name="lock_icon_bg" />
     <item type="id" name="burn_in_layer" />
-    <item type="id" name="burn_in_layer_empty_view" />
     <item type="id" name="communal_tutorial_indicator" />
     <item type="id" name="nssl_placeholder_barrier_bottom" />
     <item type="id" name="ambient_indication_container" />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b713417..5dbdd18 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -75,6 +75,11 @@
     <!-- Battery saver notification dismiss action: Do not turn on battery saver. [CHAR LIMIT=NONE]-->
     <string name="battery_saver_dismiss_action">No thanks</string>
 
+    <!-- Secondary label for Battery Saver tile when Battery Saver is enabled. [CHAR LIMIT=20] -->
+    <string name="standard_battery_saver_text">Standard</string>
+    <!-- Secondary label for Battery Saver tile when Extreme Battery Saver is enabled. [CHAR LIMIT=20] -->
+    <string name="extreme_battery_saver_text">Extreme</string>
+
     <!-- Name of the button that links to the Settings app. [CHAR LIMIT=NONE] -->
 
     <!-- Name of the button that links to the Wifi settings screen. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index f28d405..8a2245d 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -404,7 +404,9 @@
                             if (nextAlarmMillis > 0) nextAlarmMillis else null,
                             SysuiR.string::status_bar_alarm.name
                         )
-                        .also { data -> clock?.run { events.onAlarmDataChanged(data) } }
+                        .also { data ->
+                            mainExecutor.execute { clock?.run { events.onAlarmDataChanged(data) } }
+                        }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index c0ae4a1..9421f15 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -53,6 +53,9 @@
 import com.android.systemui.animation.ViewHierarchyAnimator;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.plugins.clocks.ClockController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.power.shared.model.ScreenPowerState;
@@ -101,6 +104,7 @@
     private final Rect mClipBounds = new Rect();
     private final KeyguardInteractor mKeyguardInteractor;
     private final PowerInteractor mPowerInteractor;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     private final DozeParameters mDozeParameters;
 
     private View mStatusArea = null;
@@ -108,6 +112,7 @@
 
     private Boolean mSplitShadeEnabled = false;
     private Boolean mStatusViewCentered = true;
+    private boolean mGoneToAodTransitionRunning = false;
     private DumpManager mDumpManager;
 
     private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener =
@@ -176,6 +181,7 @@
             KeyguardLogger logger,
             InteractionJankMonitor interactionJankMonitor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
             DumpManager dumpManager,
             PowerInteractor powerInteractor) {
         super(keyguardStatusView);
@@ -191,6 +197,7 @@
         mDumpManager = dumpManager;
         mKeyguardInteractor = keyguardInteractor;
         mPowerInteractor = powerInteractor;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
     }
 
     @Override
@@ -225,7 +232,6 @@
         mDumpManager.registerDumpable(getInstanceName(), this);
         if (migrateClocksToBlueprint()) {
             startCoroutines(EmptyCoroutineContext.INSTANCE);
-            mView.setVisibility(View.GONE);
         }
     }
 
@@ -241,6 +247,15 @@
                         dozeTimeTick();
                     }
                 }, context);
+
+        collectFlow(mView, mKeyguardTransitionInteractor.getGoneToAodTransition(),
+                (TransitionStep step) -> {
+                    if (step.getTransitionState() == TransitionState.RUNNING) {
+                        mGoneToAodTransitionRunning = true;
+                    } else {
+                        mGoneToAodTransitionRunning = false;
+                    }
+                }, context);
     }
 
     public KeyguardStatusView getView() {
@@ -311,7 +326,7 @@
      * Set keyguard status view alpha.
      */
     public void setAlpha(float alpha) {
-        if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) {
+        if (!mKeyguardVisibilityHelper.isVisibilityAnimating() && !mGoneToAodTransitionRunning) {
             mView.setAlpha(alpha);
         }
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 77054bd..2000028 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -88,10 +88,6 @@
             boolean keyguardFadingAway,
             boolean goingToFullShade,
             int oldStatusBarState) {
-        if (migrateClocksToBlueprint()) {
-            log("Ignoring all of KeyguardVisibilityelper");
-            return;
-        }
         Assert.isMainThread();
         PropertyAnimator.cancelAnimation(mView, AnimatableProperty.ALPHA);
         boolean isOccluded = mKeyguardStateController.isOccluded();
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 8853589..33f14d4 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -22,17 +22,13 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
-import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.Preconditions;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
-import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 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.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentService;
@@ -47,7 +43,6 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationMediaManager;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
@@ -58,7 +53,6 @@
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.FlashlightController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.tuner.TunablePadding.TunablePaddingService;
 import com.android.systemui.tuner.TunerService;
@@ -70,6 +64,7 @@
 import javax.inject.Inject;
 import javax.inject.Named;
 
+
 /**
  * Class to handle ugly dependencies throughout sysui until we determine the
  * long-term dependency injection solution.
@@ -96,10 +91,6 @@
      * Key for getting a Handler for receiving time tick broadcasts on.
      */
     public static final String TIME_TICK_HANDLER_NAME = "time_tick_handler";
-    /**
-     * Generic handler on the main thread.
-     */
-    private static final String MAIN_HANDLER_NAME = "main_handler";
 
     /**
      * An email address to send memory leak reports to by default.
@@ -121,11 +112,6 @@
      */
     public static final DependencyKey<Handler> TIME_TICK_HANDLER =
             new DependencyKey<>(TIME_TICK_HANDLER_NAME);
-    /**
-     * Generic handler on the main thread.
-     */
-    public static final DependencyKey<Handler> MAIN_HANDLER =
-            new DependencyKey<>(MAIN_HANDLER_NAME);
 
     private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>();
     private final ArrayMap<Object, LazyDependencyCreator> mProviders = new ArrayMap<>();
@@ -134,7 +120,6 @@
 
     @Inject Lazy<BroadcastDispatcher> mBroadcastDispatcher;
     @Inject Lazy<BluetoothController> mBluetoothController;
-    @Inject Lazy<FlashlightController> mFlashlightController;
     @Inject Lazy<KeyguardUpdateMonitor> mKeyguardUpdateMonitor;
     @Inject Lazy<DeviceProvisionedController> mDeviceProvisionedController;
     @Inject Lazy<PluginManager> mPluginManager;
@@ -150,15 +135,10 @@
     @Inject Lazy<LightBarController> mLightBarController;
     @Inject Lazy<OverviewProxyService> mOverviewProxyService;
     @Inject Lazy<NavigationModeController> mNavBarModeController;
-    @Inject Lazy<AccessibilityButtonModeObserver> mAccessibilityButtonModeObserver;
-    @Inject Lazy<AccessibilityButtonTargetsObserver> mAccessibilityButtonListController;
-    @Inject Lazy<IStatusBarService> mIStatusBarService;
-    @Inject Lazy<NotificationRemoteInputManager.Callback> mNotificationRemoteInputManagerCallback;
     @Inject Lazy<NavigationBarController> mNavigationBarController;
     @Inject Lazy<StatusBarStateController> mStatusBarStateController;
     @Inject Lazy<NotificationMediaManager> mNotificationMediaManager;
     @Inject @Background Lazy<Looper> mBgLooper;
-    @Inject @Main Lazy<Handler> mMainHandler;
     @Inject @Named(TIME_TICK_HANDLER_NAME) Lazy<Handler> mTimeTickHandler;
     @Inject Lazy<SysUiState> mSysUiStateFlagsContainer;
     @Inject Lazy<CommandQueue> mCommandQueue;
@@ -187,10 +167,8 @@
         // on imports.
         mProviders.put(TIME_TICK_HANDLER, mTimeTickHandler::get);
         mProviders.put(BG_LOOPER, mBgLooper::get);
-        mProviders.put(MAIN_HANDLER, mMainHandler::get);
         mProviders.put(BroadcastDispatcher.class, mBroadcastDispatcher::get);
         mProviders.put(BluetoothController.class, mBluetoothController::get);
-        mProviders.put(FlashlightController.class, mFlashlightController::get);
         mProviders.put(KeyguardUpdateMonitor.class, mKeyguardUpdateMonitor::get);
         mProviders.put(DeviceProvisionedController.class, mDeviceProvisionedController::get);
         mProviders.put(PluginManager.class, mPluginManager::get);
@@ -205,13 +183,6 @@
         mProviders.put(LightBarController.class, mLightBarController::get);
         mProviders.put(OverviewProxyService.class, mOverviewProxyService::get);
         mProviders.put(NavigationModeController.class, mNavBarModeController::get);
-        mProviders.put(AccessibilityButtonModeObserver.class,
-                mAccessibilityButtonModeObserver::get);
-        mProviders.put(AccessibilityButtonTargetsObserver.class,
-                mAccessibilityButtonListController::get);
-        mProviders.put(IStatusBarService.class, mIStatusBarService::get);
-        mProviders.put(NotificationRemoteInputManager.Callback.class,
-                mNotificationRemoteInputManagerCallback::get);
         mProviders.put(NavigationBarController.class, mNavigationBarController::get);
         mProviders.put(StatusBarStateController.class, mStatusBarStateController::get);
         mProviders.put(NotificationMediaManager.class, mNotificationMediaManager::get);
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 31698a3..01c2cc4 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -345,11 +345,25 @@
         }
     }
 
-    private TextView loadPercentView() {
+    private TextView inflatePercentView() {
         return (TextView) LayoutInflater.from(getContext())
                 .inflate(R.layout.battery_percentage_view, null);
     }
 
+    private void addPercentView(TextView inflatedPercentView) {
+        mBatteryPercentView = inflatedPercentView;
+
+        if (mPercentageStyleId != 0) { // Only set if specified as attribute
+            mBatteryPercentView.setTextAppearance(mPercentageStyleId);
+        }
+        float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null);
+        mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight);
+        if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
+        addView(mBatteryPercentView, new LayoutParams(
+                LayoutParams.WRAP_CONTENT,
+                (int) Math.ceil(fontHeight)));
+    }
+
     /**
      * Updates percent view by removing old one and reinflating if necessary
      */
@@ -388,7 +402,9 @@
             mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
                     (String estimate) -> {
                         if (mBatteryPercentView == null) {
-                            mBatteryPercentView = loadPercentView();
+                            // Similar to the legacy behavior, inflate and add the view. We will
+                            // only use it for the estimate text
+                            addPercentView(inflatePercentView());
                         }
                         if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
                             mEstimateText = estimate;
@@ -401,6 +417,10 @@
                         }
                     });
         } else {
+            if (mBatteryPercentView != null) {
+                mEstimateText = null;
+                mBatteryPercentView.setText(null);
+            }
             updateContentDescription();
         }
     }
@@ -485,22 +505,19 @@
             return;
         }
 
-        if (mUnifiedBattery == null) {
-            return;
+        if (!mShowPercentAvailable || mUnifiedBattery == null) return;
+
+        boolean shouldShow = mShowPercentMode == MODE_ON || mShowPercentMode == MODE_ESTIMATE;
+        if (!mBatteryStateUnknown && !shouldShow && (mShowPercentMode != MODE_OFF)) {
+            // Slow case: fall back to the system setting
+            // TODO(b/140051051)
+            shouldShow = 0 != whitelistIpcs(() -> Settings.System
+                    .getIntForUser(getContext().getContentResolver(),
+                    SHOW_BATTERY_PERCENT, getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.config_defaultBatteryPercentageSetting)
+                    ? 1 : 0, UserHandle.USER_CURRENT));
         }
 
-        // TODO(b/140051051)
-        final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
-                .getIntForUser(getContext().getContentResolver(),
-                SHOW_BATTERY_PERCENT, getContext().getResources().getBoolean(
-                com.android.internal.R.bool.config_defaultBatteryPercentageSetting)
-                ? 1 : 0, UserHandle.USER_CURRENT));
-
-        boolean shouldShow =
-                (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
-                        || mShowPercentMode == MODE_ON;
-        shouldShow = shouldShow && !mBatteryStateUnknown;
-
         setBatteryDrawableState(
                 new BatteryDrawableState(
                         mUnifiedBatteryState.getLevel(),
@@ -534,17 +551,8 @@
 
         if (shouldShow) {
             if (!showing) {
-                mBatteryPercentView = loadPercentView();
-                if (mPercentageStyleId != 0) { // Only set if specified as attribute
-                    mBatteryPercentView.setTextAppearance(mPercentageStyleId);
-                }
-                float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null);
-                mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight);
-                if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
+                addPercentView(inflatePercentView());
                 updatePercentText();
-                addView(mBatteryPercentView, new LayoutParams(
-                        LayoutParams.WRAP_CONTENT,
-                        (int) Math.ceil(fontHeight)));
             }
         } else {
             if (showing) {
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
index 8c9ae62..10a0d95 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
@@ -1,9 +1,21 @@
 package com.android.systemui.battery
 
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.BatterySaverTile
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileDataInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.impl.battery.ui.BatterySaverTileMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 import dagger.multibindings.IntoMap
 import dagger.multibindings.StringKey
 
@@ -15,4 +27,39 @@
     @IntoMap
     @StringKey(BatterySaverTile.TILE_SPEC)
     fun bindBatterySaverTile(batterySaverTile: BatterySaverTile): QSTileImpl<*>
+
+    companion object {
+        private const val BATTERY_SAVER_TILE_SPEC = "battery"
+
+        @Provides
+        @IntoMap
+        @StringKey(BATTERY_SAVER_TILE_SPEC)
+        fun provideBatterySaverTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(BATTERY_SAVER_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.qs_battery_saver_icon_off,
+                        labelRes = R.string.battery_detail_switch_title,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject BatterySaverTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(BATTERY_SAVER_TILE_SPEC)
+        fun provideBatterySaverTileViewModel(
+            factory: QSTileViewModelFactory.Static<BatterySaverTileModel>,
+            mapper: BatterySaverTileMapper,
+            stateInteractor: BatterySaverTileDataInteractor,
+            userActionInteractor: BatterySaverTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(BATTERY_SAVER_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt
index 1b8495a..f3652b8 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt
@@ -23,6 +23,7 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.DrawableWrapper
 import android.view.Gravity
+import kotlin.math.ceil
 import kotlin.math.min
 import kotlin.math.roundToInt
 
@@ -36,7 +37,7 @@
  */
 @Suppress("RtlHardcoded")
 class BatteryAttributionDrawable(dr: Drawable?) : DrawableWrapper(dr) {
-    /** One of [CENTER, LEFT]. Note that RTL is handled in the parent */
+    /** One of [CENTER, LEFT]. Note that number text does not RTL. */
     var gravity = Gravity.CENTER
         set(value) {
             field = value
@@ -67,8 +68,8 @@
             dr.setBounds(
                 bounds.left,
                 bounds.top,
-                (bounds.left + dw).roundToInt(),
-                (bounds.top + dh).roundToInt()
+                ceil(bounds.left + dw).toInt(),
+                ceil(bounds.top + dh).toInt()
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt
index 6d32067..5e34d29 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt
@@ -26,6 +26,7 @@
 import android.graphics.Rect
 import android.graphics.RectF
 import android.graphics.drawable.Drawable
+import android.view.View
 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
 import kotlin.math.floor
 import kotlin.math.roundToInt
@@ -103,6 +104,11 @@
         // saveLayer is needed here so we don't clip the other layers of our drawable
         canvas.saveLayer(null, null)
 
+        // Fill from the opposite direction in rtl mode
+        if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+            canvas.scale(-1f, 1f, bounds.width() / 2f, bounds.height() / 2f)
+        }
+
         // We need to use 3 draw commands:
         // 1. Clip to the current level
         // 2. Clip anything outside of the path
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt
index 199dd1f..706b9ec 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt
@@ -26,7 +26,10 @@
 import android.graphics.drawable.LayerDrawable
 import android.util.PathParser
 import android.view.Gravity
+import android.view.View
 import com.android.systemui.res.R
+import kotlin.math.ceil
+import kotlin.math.floor
 import kotlin.math.roundToInt
 
 /**
@@ -69,8 +72,11 @@
 ) : LayerDrawable(arrayOf(frameBg, frame, fill, textOnly, spaceSharingText, attribution)) {
 
     private val scaleMatrix = Matrix().also { it.setScale(1f, 1f) }
-    private val scaledAttrFullCanvas = RectF(Metrics.AttrFullCanvas)
-    private val scaledAttrRightCanvas = RectF(Metrics.AttrRightCanvas)
+
+    private val attrFullCanvas = RectF()
+    private val attrRightCanvas = RectF()
+    private val scaledAttrFullCanvas = RectF()
+    private val scaledAttrRightCanvas = RectF()
 
     var batteryState = batteryState
         set(value) {
@@ -88,6 +94,12 @@
             updateColors(batteryState.showErrorState, value)
         }
 
+    init {
+        isAutoMirrored = true
+        // Initialize the canvas rects since they are not static
+        setAttrRects(layoutDirection == View.LAYOUT_DIRECTION_RTL)
+    }
+
     private fun handleUpdateState(old: BatteryDrawableState, new: BatteryDrawableState) {
         if (new.showErrorState != old.showErrorState) {
             updateColors(new.showErrorState, colors)
@@ -144,9 +156,42 @@
             bounds.height() / Metrics.ViewportHeight
         )
 
-        // Scale the attribution bounds
-        scaleMatrix.mapRect(scaledAttrFullCanvas, Metrics.AttrFullCanvas)
-        scaleMatrix.mapRect(scaledAttrRightCanvas, Metrics.AttrRightCanvas)
+        scaleAttributionBounds()
+    }
+
+    override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
+        setAttrRects(layoutDirection == View.LAYOUT_DIRECTION_RTL)
+        scaleAttributionBounds()
+
+        return super.onLayoutDirectionChanged(layoutDirection)
+    }
+
+    private fun setAttrRects(rtl: Boolean) {
+        // Local refs make the math easier to parse
+        val full = Metrics.AttrFullCanvasInsets
+        val side = Metrics.AttrRightCanvasInsets
+        val sideRtl = Metrics.AttrRightCanvasInsetsRtl
+        val vh = Metrics.ViewportHeight
+        val vw = Metrics.ViewportWidth
+
+        attrFullCanvas.set(
+            if (rtl) full.right else full.left,
+            full.top,
+            vw - if (rtl) full.left else full.right,
+            vh - full.bottom,
+        )
+        attrRightCanvas.set(
+            if (rtl) sideRtl.left else side.left,
+            side.top,
+            vw - (if (rtl) sideRtl.right else side.right),
+            vh - side.bottom,
+        )
+    }
+
+    /** If bounds (i.e., scale), or RTL properties change, we have to recalculate the attr bounds */
+    private fun scaleAttributionBounds() {
+        scaleMatrix.mapRect(scaledAttrFullCanvas, attrFullCanvas)
+        scaleMatrix.mapRect(scaledAttrRightCanvas, attrRightCanvas)
     }
 
     override fun draw(canvas: Canvas) {
@@ -163,13 +208,14 @@
         if (batteryState.showPercent && batteryState.attribution != null) {
             // 4a. percent & attribution. Implies space-sharing
 
-            // Configure the attribute to draw in a smaller bounding box and align left
+            // Configure the attribute to draw in a smaller bounding box and align left and use
+            // floor/ceil math to make sure we get every available pixel
             attribution.gravity = Gravity.LEFT
             attribution.setBounds(
-                scaledAttrRightCanvas.left.roundToInt(),
-                scaledAttrRightCanvas.top.roundToInt(),
-                scaledAttrRightCanvas.right.roundToInt(),
-                scaledAttrRightCanvas.bottom.roundToInt(),
+                floor(scaledAttrRightCanvas.left).toInt(),
+                floor(scaledAttrRightCanvas.top).toInt(),
+                ceil(scaledAttrRightCanvas.right).toInt(),
+                ceil(scaledAttrRightCanvas.bottom).toInt(),
             )
             attribution.draw(canvas)
 
@@ -196,16 +242,44 @@
      */
     override fun setAlpha(alpha: Int) {}
 
+    /**
+     * Interface that describes relevant top-level metrics for the proper rendering of this icon.
+     * The overall canvas is defined as ViewportWidth x ViewportHeight, which is hard coded to 24x14
+     * points.
+     *
+     * The attr canvas insets are rect inset definitions. That is, they are defined as l,t,r,b
+     * points from the nearest edge. Note that for RTL, we don't actually flip the text since
+     * numbers do not reverse for RTL locales.
+     */
     interface M {
         val ViewportWidth: Float
         val ViewportHeight: Float
 
-        // Bounds, oriented in the above viewport, where we will fit-center and center-align
-        // an attribution that is the sole foreground element
-        val AttrFullCanvas: RectF
-        // Bounds, oriented in the above viewport, where we will fit-center and left-align
-        // an attribution that is sharing space with the percent text of the drawable
-        val AttrRightCanvas: RectF
+        /**
+         * Insets, oriented in the above viewport in LTR, that define the full canvas for a single
+         * foreground element. The element will be fit-center and center-aligned on this canvas
+         *
+         * 18x8 point size
+         */
+        val AttrFullCanvasInsets: RectF
+
+        /**
+         * Insets, oriented in the above viewport in LTR, that define the partial canvas for a
+         * foreground element that shares space with the percent text. The element will be
+         * fit-center and left-aligned on this canvas.
+         *
+         * 6x6 point size
+         */
+        val AttrRightCanvasInsets: RectF
+
+        /**
+         * Insets, oriented in the above viewport in RTL, that define the partial canvas for a
+         * foreground element that shares space with the percent text. The element will be
+         * fit-center and left-aligned on this canvas.
+         *
+         * 6x6 point size
+         */
+        val AttrRightCanvasInsetsRtl: RectF
     }
 
     companion object {
@@ -220,20 +294,9 @@
                 override val ViewportWidth: Float = 24f
                 override val ViewportHeight: Float = 14f
 
-                /**
-                 * Bounds, oriented in the above viewport, where we will fit-center and center-align
-                 * an attribution that is the sole foreground element
-                 *
-                 * 18x8 point size
-                 */
-                override val AttrFullCanvas: RectF = RectF(4f, 3f, 22f, 11f)
-                /**
-                 * Bounds, oriented in the above viewport, where we will fit-center and left-align
-                 * an attribution that is sharing space with the percent text of the drawable
-                 *
-                 * 6x6 point size
-                 */
-                override val AttrRightCanvas: RectF = RectF(16f, 4f, 22f, 10f)
+                override val AttrFullCanvasInsets = RectF(4f, 3f, 2f, 3f)
+                override val AttrRightCanvasInsets = RectF(16f, 4f, 2f, 4f)
+                override val AttrRightCanvasInsetsRtl = RectF(14f, 4f, 4f, 4f)
             }
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt
index 123d6ba..aa0e373 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt
@@ -23,6 +23,7 @@
 import android.graphics.Rect
 import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.view.View
 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
 
 /**
@@ -71,6 +72,7 @@
     }
 
     override fun draw(canvas: Canvas) {
+        val rtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
         val totalAvailableHeight = CanvasHeight * vScale
 
         // Distribute the vertical whitespace around the text. This is a simplified version of
@@ -81,11 +83,12 @@
         val totalAvailableWidth = CanvasWidth * hScale
         val textWidth = textPaint.measureText(percentText)
         val offsetX = (totalAvailableWidth - textWidth) / 2
+        val startOffset = if (rtl) ViewportInsetRight else ViewportInsetLeft
 
         // Draw the text centered in the available area
         canvas.drawText(
             percentText,
-            (ViewportInsetLeft * hScale) + offsetX,
+            (startOffset * hScale) + offsetX,
             (ViewportInsetTop * vScale) + offsetY,
             textPaint
         )
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt
index 0c418b9..3b4c779 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt
@@ -23,6 +23,7 @@
 import android.graphics.Rect
 import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.view.View
 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
 
 /**
@@ -94,6 +95,7 @@
     }
 
     override fun draw(canvas: Canvas) {
+        val rtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
         val totalAvailableHeight = CanvasHeight * vScale
 
         // Distribute the vertical whitespace around the text. This is a simplified version of
@@ -107,7 +109,7 @@
 
         canvas.drawText(
             percentText,
-            (ViewportInsetLeft * hScale) + offsetX,
+            ((if (rtl) ViewportInsetLeftRtl else ViewportInsetLeft) * hScale) + offsetX,
             (ViewportInsetTop * vScale) + offsetY,
             textPaint
         )
@@ -128,6 +130,7 @@
 
     companion object {
         private const val ViewportInsetLeft = 4f
+        private const val ViewportInsetLeftRtl = 2f
         private const val ViewportInsetTop = 2f
 
         private const val CanvasWidth = 12f
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
index 6af0fa0..66aeda6 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
@@ -42,7 +42,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.controls.util.MediaDataUtils;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
@@ -69,7 +69,7 @@
 
     private final Context mContext;
     private final UiEventLogger mUiEventLogger;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
     private final LocalBluetoothManager mLocalBluetoothManager;
     private final BroadcastSender mBroadcastSender;
     private final SystemUIDialog.Factory mSystemUIDialogFactory;
@@ -157,7 +157,7 @@
     @AssistedInject
     BroadcastDialogDelegate(
             Context context,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             @Nullable LocalBluetoothManager localBluetoothManager,
             UiEventLogger uiEventLogger,
             @Background Executor bgExecutor,
@@ -166,7 +166,7 @@
             @Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp,
             @Assisted(OUTPUT_PKG_NAME) String outputPkgName) {
         mContext = context;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
         mLocalBluetoothManager = localBluetoothManager;
         mSystemUIDialogFactory = systemUIDialogFactory;
         mCurrentBroadcastApp = currentBroadcastApp;
@@ -218,7 +218,7 @@
                 R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp), null);
         mSwitchBroadcast.setOnClickListener((view) -> startSwitchBroadcast());
         changeOutput.setOnClickListener((view) -> {
-            mMediaOutputDialogFactory.create(mOutputPackageName, true, null);
+            mMediaOutputDialogManager.createAndShow(mOutputPackageName, true, null);
             dialog.dismiss();
         });
         cancelBtn.setOnClickListener((view) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 1c8b84d..b42eda1 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -73,6 +73,14 @@
                 initialValue = isInputEnabled.value && !isTextFieldFocused.value,
             )
 
+    /** The ID of the currently-selected user. */
+    val selectedUserId: StateFlow<Int> =
+        selectedUserInteractor.selectedUser.stateIn(
+            scope = viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = selectedUserInteractor.getSelectedUserId(),
+        )
+
     override fun onHidden() {
         super.onHidden()
         isTextFieldFocused.value = false
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 35b27aa..fc9a7df 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
@@ -21,7 +21,6 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
@@ -47,7 +46,6 @@
 import kotlinx.coroutines.launch
 
 /** The default view model used for showing the communal hub. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class CommunalViewModel
 @Inject
@@ -56,7 +54,6 @@
     private val communalInteractor: CommunalInteractor,
     tutorialInteractor: CommunalTutorialInteractor,
     shadeInteractor: ShadeInteractor,
-    deviceEntryInteractor: DeviceEntryInteractor,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
     @CommunalLog logBuffer: LogBuffer,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
@@ -90,8 +87,6 @@
     /** Whether touches should be disabled in communal */
     val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
 
-    val deviceUnlocked: Flow<Boolean> = deviceEntryInteractor.isUnlocked
-
     init {
         // Initialize our media host for the UMO. This only needs to happen once and must be done
         // before the MediaHierarchyManager attempts to move the UMO to the hub.
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
index 79455eb..289dbd9 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
@@ -24,9 +24,12 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.shared.model.BiometricMessage
 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -60,17 +63,23 @@
     private val context: Context,
     activityStarter: ActivityStarter,
     powerInteractor: PowerInteractor,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
     private val keyguardOccludedByApp: Flow<Boolean> =
-        combine(
-                keyguardInteractor.isKeyguardOccluded,
-                keyguardInteractor.isKeyguardShowing,
-                primaryBouncerInteractor.isShowing,
-                alternateBouncerInteractor.isVisible,
-            ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
-                occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
-            }
-            .distinctUntilChanged()
+        if (KeyguardWmStateRefactor.isEnabled) {
+            keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.OCCLUDED }
+        } else {
+            combine(
+                    keyguardInteractor.isKeyguardOccluded,
+                    keyguardInteractor.isKeyguardShowing,
+                    primaryBouncerInteractor.isShowing,
+                    alternateBouncerInteractor.isVisible,
+                ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
+                    occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
+                }
+                .distinctUntilChanged()
+        }
+
     private val fingerprintUnlockSuccessEvents: Flow<Unit> =
         fingerprintAuthRepository.authenticationStatus
             .ifKeyguardOccludedByApp()
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 c0a873a..989b0de 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
@@ -18,6 +18,7 @@
 import android.content.Context
 import android.os.Bundle
 import android.view.View
+import android.view.WindowInsets
 import android.widget.TextView
 import androidx.core.view.updatePadding
 import com.android.systemui.res.R
@@ -44,7 +45,10 @@
     private lateinit var mirrorButton: TextView
     private lateinit var dismissButton: TextView
     private lateinit var dualDisplayWarning: TextView
+    private lateinit var bottomSheet: View
     private var enabledPressed = false
+    private val defaultDialogBottomInset =
+        context.resources.getDimensionPixelSize(R.dimen.dialog_bottom_padding)
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -63,6 +67,8 @@
                 visibility = if (showConcurrentDisplayInfo) View.VISIBLE else View.GONE
             }
 
+        bottomSheet = requireViewById(R.id.cd_bottom_sheet)
+
         setOnDismissListener {
             if (!enabledPressed) {
                 onCancelMirroring.onClick(null)
@@ -71,15 +77,17 @@
         setupInsets()
     }
 
-    private fun setupInsets() {
+    private fun setupInsets(navbarInsets: Int = navbarBottomInsetsProvider()) {
         // This avoids overlap between dialog content and navigation bars.
-        requireViewById<View>(R.id.cd_bottom_sheet).apply {
-            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, defaultDialogBottomInset))
+        // 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.
+        bottomSheet.updatePadding(bottom = max(navbarInsets, defaultDialogBottomInset))
+    }
+
+    override fun onInsetsChanged(changedTypes: Int, insets: WindowInsets) {
+        val navbarType = WindowInsets.Type.navigationBars()
+        if (changedTypes and navbarType != 0) {
+            setupInsets(insets.getInsets(navbarType).bottom)
         }
     }
 
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 190062c..fbf0538 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
@@ -32,9 +32,12 @@
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.launch
@@ -57,6 +60,7 @@
     private var dialog: Dialog? = null
 
     /** Starts listening for pending displays. */
+    @OptIn(FlowPreview::class)
     override fun start() {
         val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay
         val concurrentDisplaysInProgessFlow =
@@ -66,6 +70,13 @@
                 flow { emit(false) }
             }
         pendingDisplayFlow
+            // Let's debounce for 2 reasons:
+            // - prevent fast dialog flashes in case pending displays are available for just a few
+            // millis
+            // - Prevent jumps related to inset changes: when in 3 buttons navigation, device
+            // unlock triggers a change in insets that might result in a jump of the dialog (if a
+            // display was connected while on the lockscreen).
+            .debounce(200.milliseconds)
             .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress
                 ->
                 if (pendingDisplay == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
index 931a869..ed82278 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
@@ -163,6 +163,6 @@
     }
 
     companion object {
-        const val KEY_UP_TIMEOUT = 100L
+        const val KEY_UP_TIMEOUT = 60L
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 2e233d8..3134e35 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -289,6 +289,8 @@
         };
     }
 
+    private final WindowManagerOcclusionManager mWmOcclusionManager;
+
     @Inject
     public KeyguardService(
             KeyguardViewMediator keyguardViewMediator,
@@ -302,7 +304,8 @@
             KeyguardSurfaceBehindParamsApplier keyguardSurfaceBehindAnimator,
             @Application CoroutineScope scope,
             FeatureFlags featureFlags,
-            PowerInteractor powerInteractor) {
+            PowerInteractor powerInteractor,
+            WindowManagerOcclusionManager windowManagerOcclusionManager) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -323,6 +326,8 @@
                     keyguardSurfaceBehindAnimator,
                     scope);
         }
+
+        mWmOcclusionManager = windowManagerOcclusionManager;
     }
 
     @Override
@@ -414,7 +419,11 @@
 
             Trace.beginSection("KeyguardService.mBinder#setOccluded");
             checkPermission();
-            mKeyguardViewMediator.setOccluded(isOccluded, animate);
+            if (!KeyguardWmStateRefactor.isEnabled()) {
+                mKeyguardViewMediator.setOccluded(isOccluded, animate);
+            } else {
+                mWmOcclusionManager.onKeyguardServiceSetOccluded(isOccluded);
+            }
             Trace.endSection();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 6d917bb..a37397d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -1365,7 +1365,9 @@
 
     private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager;
 
+    private WindowManagerOcclusionManager mWmOcclusionManager;
     /**
+
      * Injected constructor. See {@link KeyguardModule}.
      */
     public KeyguardViewMediator(
@@ -1411,7 +1413,8 @@
             SystemPropertiesHelper systemPropertiesHelper,
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
-            KeyguardInteractor keyguardInteractor) {
+            KeyguardInteractor keyguardInteractor,
+            WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
         mFalsingCollector = falsingCollector;
@@ -1486,6 +1489,8 @@
 
         mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard");
         mShowKeyguardWakeLock.setReferenceCounted(false);
+
+        mWmOcclusionManager = wmOcclusionManager;
     }
 
     public void userActivity() {
@@ -2103,15 +2108,27 @@
     }
 
     public IRemoteAnimationRunner getOccludeAnimationRunner() {
-        return validatingRemoteAnimationRunner(mOccludeAnimationRunner);
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            return validatingRemoteAnimationRunner(mWmOcclusionManager.getOccludeAnimationRunner());
+        } else {
+            return validatingRemoteAnimationRunner(mOccludeAnimationRunner);
+        }
     }
 
+    /**
+     * TODO(b/326464548): Move this to WindowManagerOcclusionManager
+     */
     public IRemoteAnimationRunner getOccludeByDreamAnimationRunner() {
         return validatingRemoteAnimationRunner(mOccludeByDreamAnimationRunner);
     }
 
     public IRemoteAnimationRunner getUnoccludeAnimationRunner() {
-        return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner);
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            return validatingRemoteAnimationRunner(
+                    mWmOcclusionManager.getUnoccludeAnimationRunner());
+        } else {
+            return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner);
+        }
     }
 
     public boolean isHiding() {
@@ -2145,8 +2162,10 @@
 
             if (mOccluded != isOccluded) {
                 mOccluded = isOccluded;
-                mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate
-                        && mDeviceInteractive);
+                if (!KeyguardWmStateRefactor.isEnabled()) {
+                    mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate
+                            && mDeviceInteractive);
+                }
                 adjustStatusBarLocked();
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
index 8ebcece..00f5002 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -140,8 +140,14 @@
         nonApps: Array<RemoteAnimationTarget>,
         finishedCallback: IRemoteAnimationFinishedCallback
     ) {
-        goingAwayRemoteAnimationFinishedCallback = finishedCallback
-        keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0])
+        if (apps.isNotEmpty()) {
+            goingAwayRemoteAnimationFinishedCallback = finishedCallback
+            keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0])
+        } else {
+            // Nothing to do here if we have no apps, end the animation, which will cancel it and WM
+            // will make *something* visible.
+            finishedCallback.onAnimationFinished()
+        }
     }
 
     fun onKeyguardGoingAwayRemoteAnimationCancelled() {
@@ -174,13 +180,19 @@
      * if so, true should be the right choice.
      */
     private fun setWmLockscreenState(
-            lockscreenShowing: Boolean = this.isLockscreenShowing ?: true.also {
-                Log.d(TAG, "Using isLockscreenShowing=true default in setWmLockscreenState, " +
-                        "because setAodVisible was called before the first setLockscreenShown " +
-                        "call during boot. This is not typical, but is theoretically possible. " +
-                        "If you're investigating the lockscreen showing unexpectedly, start here.")
-            },
-            aodVisible: Boolean = this.isAodVisible
+        lockscreenShowing: Boolean =
+            this.isLockscreenShowing
+                ?: true.also {
+                    Log.d(
+                        TAG,
+                        "Using isLockscreenShowing=true default in setWmLockscreenState, " +
+                            "because setAodVisible was called before the first " +
+                            "setLockscreenShown call during boot. This is not typical, but is " +
+                            "theoretically possible. If you're investigating the lockscreen " +
+                            "showing unexpectedly, start here."
+                    )
+                },
+        aodVisible: Boolean = this.isAodVisible
     ) {
         Log.d(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
new file mode 100644
index 0000000..aab90c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Matrix
+import android.os.RemoteException
+import android.util.Log
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SyncRtSurfaceTransactionApplier
+import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.TransitionAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.res.R
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private val UNOCCLUDE_ANIMATION_DURATION = 250
+private val UNOCCLUDE_TRANSLATE_DISTANCE_PERCENT = 0.1f
+
+/**
+ * Keeps track of Window Manager's occlusion state and RemoteAnimations related to changes in
+ * occlusion state. Occlusion is when a [FLAG_SHOW_WHEN_LOCKED] activity is displaying over the
+ * lockscreen - we're still locked, but the user can interact with the activity.
+ *
+ * Typical "occlusion" use cases include launching the camera over the lockscreen, tapping a quick
+ * affordance to bring up Google Pay/Wallet/whatever it's called by the time you're reading this,
+ * and Maps Navigation.
+ *
+ * Window Manager considers the keyguard to be 'occluded' whenever a [FLAG_SHOW_WHEN_LOCKED]
+ * activity is on top of the task stack, even if the device is unlocked and the keyguard is not
+ * visible. System UI considers the keyguard to be [KeyguardState.OCCLUDED] only when we're on the
+ * keyguard and an activity is displaying over it.
+ *
+ * For all System UI use cases, you should use [KeyguardTransitionInteractor] to determine if we're
+ * in the [KeyguardState.OCCLUDED] state and react accordingly. If you are sure that you need to
+ * check whether Window Manager considers OCCLUDED=true even though the lockscreen is not showing,
+ * use [KeyguardShowWhenLockedActivityInteractor.isShowWhenLockedActivityOnTop] in combination with
+ * [KeyguardTransitionInteractor] state.
+ *
+ * This is a very sensitive piece of state that has caused many headaches in the past. Please be
+ * careful.
+ */
+@SysUISingleton
+class WindowManagerOcclusionManager
+@Inject
+constructor(
+    val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    val activityTransitionAnimator: ActivityTransitionAnimator,
+    val keyguardViewController: dagger.Lazy<KeyguardViewController>,
+    val powerInteractor: PowerInteractor,
+    val context: Context,
+    val interactionJankMonitor: InteractionJankMonitor,
+    @Main executor: Executor,
+    val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    val occlusionInteractor: KeyguardOcclusionInteractor,
+) {
+    val powerButtonY =
+        context.resources.getDimensionPixelSize(
+            R.dimen.physical_power_button_center_screen_location_y
+        )
+    val windowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+
+    var occludeAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null
+
+    /**
+     * Animation runner provided to WindowManager, which will be used if an occluding activity is
+     * launched and Window Manager wants us to animate it in. This is used as a signal that we are
+     * now occluded, and should update our state accordingly.
+     */
+    val occludeAnimationRunner: IRemoteAnimationRunner =
+        object : IRemoteAnimationRunner.Stub() {
+            override fun onAnimationStart(
+                transit: Int,
+                apps: Array<RemoteAnimationTarget>,
+                wallpapers: Array<RemoteAnimationTarget>,
+                nonApps: Array<RemoteAnimationTarget>,
+                finishedCallback: IRemoteAnimationFinishedCallback?
+            ) {
+                Log.d(TAG, "occludeAnimationRunner#onAnimationStart")
+                // Wrap the callback so that it's guaranteed to be nulled out once called.
+                occludeAnimationFinishedCallback =
+                    object : IRemoteAnimationFinishedCallback.Stub() {
+                        override fun onAnimationFinished() {
+                            finishedCallback?.onAnimationFinished()
+                            occludeAnimationFinishedCallback = null
+                        }
+                    }
+                keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+                    showWhenLockedActivityOnTop = true,
+                    taskInfo = apps.firstOrNull()?.taskInfo,
+                )
+                activityTransitionAnimator
+                    .createRunner(occludeAnimationController)
+                    .onAnimationStart(
+                        transit,
+                        apps,
+                        wallpapers,
+                        nonApps,
+                        occludeAnimationFinishedCallback,
+                    )
+            }
+
+            override fun onAnimationCancelled() {
+                Log.d(TAG, "occludeAnimationRunner#onAnimationCancelled")
+            }
+        }
+
+    var unoccludeAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null
+
+    /**
+     * Animation runner provided to WindowManager, which will be used if an occluding activity is
+     * finished and Window Manager wants us to animate it out. This is used as a signal that we are
+     * no longer occluded, and should update our state accordingly.
+     *
+     * TODO(b/326464548): Restore dream specific animation.
+     */
+    val unoccludeAnimationRunner: IRemoteAnimationRunner =
+        object : IRemoteAnimationRunner.Stub() {
+            var unoccludeAnimator: ValueAnimator? = null
+            val unoccludeMatrix = Matrix()
+
+            /** TODO(b/326470033): Extract this logic into ViewModels. */
+            override fun onAnimationStart(
+                transit: Int,
+                apps: Array<RemoteAnimationTarget>,
+                wallpapers: Array<RemoteAnimationTarget>,
+                nonApps: Array<RemoteAnimationTarget>,
+                finishedCallback: IRemoteAnimationFinishedCallback?
+            ) {
+                Log.d(TAG, "unoccludeAnimationRunner#onAnimationStart")
+                // Wrap the callback so that it's guaranteed to be nulled out once called.
+                unoccludeAnimationFinishedCallback =
+                    object : IRemoteAnimationFinishedCallback.Stub() {
+                        override fun onAnimationFinished() {
+                            finishedCallback?.onAnimationFinished()
+                            unoccludeAnimationFinishedCallback = null
+                        }
+                    }
+                keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+                    showWhenLockedActivityOnTop = false,
+                    taskInfo = apps.firstOrNull()?.taskInfo,
+                )
+                interactionJankMonitor.begin(
+                    createInteractionJankMonitorConf(
+                        InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION,
+                        "UNOCCLUDE"
+                    )
+                )
+                if (apps.isEmpty()) {
+                    Log.d(
+                        TAG,
+                        "No apps provided to unocclude runner; " +
+                            "skipping animation and unoccluding."
+                    )
+                    unoccludeAnimationFinishedCallback?.onAnimationFinished()
+                    return
+                }
+                val target = apps[0]
+                val localView: View = keyguardViewController.get().getViewRootImpl().getView()
+                val applier = SyncRtSurfaceTransactionApplier(localView)
+                // TODO(
+                executor.execute {
+                    unoccludeAnimator?.cancel()
+                    unoccludeAnimator =
+                        ValueAnimator.ofFloat(1f, 0f).apply {
+                            duration = UNOCCLUDE_ANIMATION_DURATION.toLong()
+                            interpolator = Interpolators.TOUCH_RESPONSE
+                            addUpdateListener { animation: ValueAnimator ->
+                                val animatedValue = animation.animatedValue as Float
+                                val surfaceHeight: Float =
+                                    target.screenSpaceBounds.height().toFloat()
+
+                                unoccludeMatrix.setTranslate(
+                                    0f,
+                                    (1f - animatedValue) *
+                                        surfaceHeight *
+                                        UNOCCLUDE_TRANSLATE_DISTANCE_PERCENT
+                                )
+
+                                SurfaceParams.Builder(target.leash)
+                                    .withAlpha(animatedValue)
+                                    .withMatrix(unoccludeMatrix)
+                                    .withCornerRadius(windowCornerRadius)
+                                    .build()
+                                    .also { applier.scheduleApply(it) }
+                            }
+                            addListener(
+                                object : AnimatorListenerAdapter() {
+                                    override fun onAnimationEnd(animation: Animator) {
+                                        try {
+                                            unoccludeAnimationFinishedCallback
+                                                ?.onAnimationFinished()
+                                            unoccludeAnimator = null
+                                            interactionJankMonitor.end(
+                                                InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION
+                                            )
+                                        } catch (e: RemoteException) {
+                                            e.printStackTrace()
+                                        }
+                                    }
+                                }
+                            )
+                            start()
+                        }
+                }
+            }
+
+            override fun onAnimationCancelled() {
+                Log.d(TAG, "unoccludeAnimationRunner#onAnimationCancelled")
+                context.mainExecutor.execute { unoccludeAnimator?.cancel() }
+                Log.d(TAG, "Unocclude animation cancelled.")
+                interactionJankMonitor.cancel(InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION)
+            }
+        }
+
+    /**
+     * Called when Window Manager tells the KeyguardService directly that we're occluded or not
+     * occluded, without starting an occlude/unocclude remote animation. This happens if occlusion
+     * state changes without an animation (such as if a SHOW_WHEN_LOCKED activity is launched while
+     * we're unlocked), or if an animation has been cancelled/interrupted and Window Manager wants
+     * to make sure that we're in the correct state.
+     */
+    fun onKeyguardServiceSetOccluded(occluded: Boolean) {
+        Log.d(TAG, "#onKeyguardServiceSetOccluded($occluded)")
+        keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(occluded)
+    }
+
+    @VisibleForTesting
+    val occludeAnimationController: ActivityTransitionAnimator.Controller =
+        object : ActivityTransitionAnimator.Controller {
+
+            override var transitionContainer: ViewGroup
+                get() = keyguardViewController.get().getViewRootImpl().view as ViewGroup
+                set(_) {
+                    // Should never be set.
+                }
+
+            /** TODO(b/326470033): Extract this logic into ViewModels. */
+            override fun createAnimatorState(): TransitionAnimator.State {
+                val fullWidth = transitionContainer.width
+                val fullHeight = transitionContainer.height
+
+                if (
+                    keyguardOcclusionInteractor.showWhenLockedActivityLaunchedFromPowerGesture.value
+                ) {
+                    val initialHeight = fullHeight / 3f
+                    val initialWidth = fullWidth / 3f
+
+                    // Start the animation near the power button, at one-third size, since the
+                    // camera was launched from the power button.
+                    return TransitionAnimator.State(
+                        top = (powerButtonY - initialHeight / 2f).toInt(),
+                        bottom = (powerButtonY + initialHeight / 2f).toInt(),
+                        left = (fullWidth - initialWidth).toInt(),
+                        right = fullWidth,
+                        topCornerRadius = windowCornerRadius,
+                        bottomCornerRadius = windowCornerRadius,
+                    )
+                } else {
+                    val initialHeight = fullHeight / 2f
+                    val initialWidth = fullWidth / 2f
+
+                    // Start the animation in the center of the screen, scaled down to half
+                    // size.
+                    return TransitionAnimator.State(
+                        top = (fullHeight - initialHeight).toInt() / 2,
+                        bottom = (initialHeight + (fullHeight - initialHeight) / 2).toInt(),
+                        left = (fullWidth - initialWidth).toInt() / 2,
+                        right = (initialWidth + (fullWidth - initialWidth) / 2).toInt(),
+                        topCornerRadius = windowCornerRadius,
+                        bottomCornerRadius = windowCornerRadius,
+                    )
+                }
+            }
+        }
+
+    private fun createInteractionJankMonitorConf(
+        cuj: Int,
+        tag: String?
+    ): InteractionJankMonitor.Configuration.Builder {
+        val builder =
+            InteractionJankMonitor.Configuration.Builder.withView(
+                cuj,
+                keyguardViewController.get().getViewRootImpl().view
+            )
+        return if (tag != null) builder.setTag(tag) else builder
+    }
+
+    companion object {
+        val TAG = "WindowManagerOcclusion"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 968c3e3..5306645 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -50,6 +50,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
+import com.android.systemui.keyguard.WindowManagerOcclusionManager;
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
@@ -163,7 +164,8 @@
             SystemPropertiesHelper systemPropertiesHelper,
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
-            KeyguardInteractor keyguardInteractor) {
+            KeyguardInteractor keyguardInteractor,
+            WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
                 uiEventLogger,
@@ -209,7 +211,8 @@
                 systemPropertiesHelper,
                 wmLockscreenVisibilityManager,
                 selectedUserInteractor,
-                keyguardInteractor);
+                keyguardInteractor,
+                windowManagerOcclusionManager);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt
new file mode 100644
index 0000000..e3654b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Information about the SHOW_WHEN_LOCKED activity that is either newly on top of the task stack, or
+ * newly not on top of the task stack.
+ */
+data class ShowWhenLockedActivityInfo(
+    /** Whether the activity is on top. If not, we're unoccluding and will be animating it out. */
+    val isOnTop: Boolean,
+
+    /**
+     * Information about the activity, which we use for transition internals and also to customize
+     * animations.
+     */
+    val taskInfo: RunningTaskInfo? = null
+) {
+    fun isDream(): Boolean {
+        return taskInfo?.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM
+    }
+}
+
+/**
+ * Maintains state about "occluding" activities - activities with FLAG_SHOW_WHEN_LOCKED, which are
+ * capable of displaying over the lockscreen while the device is still locked (such as Google Maps
+ * navigation).
+ *
+ * Window Manager considers the device to be in the "occluded" state whenever such an activity is on
+ * top of the task stack, including while we're unlocked, while keyguard code considers us to be
+ * occluded only when we're locked, with an occluding activity currently displaying over the
+ * lockscreen.
+ *
+ * This dual definition is confusing, so this repository collects all of the signals WM gives us,
+ * and consolidates them into [showWhenLockedActivityInfo.isOnTop], which is the actual question WM
+ * is answering when they say whether we're 'occluded'. Keyguard then uses this signal to
+ * conditionally transition to [KeyguardState.OCCLUDED] where appropriate.
+ */
+@SysUISingleton
+class KeyguardOcclusionRepository @Inject constructor() {
+    val showWhenLockedActivityInfo = MutableStateFlow(ShowWhenLockedActivityInfo(isOnTop = false))
+
+    /**
+     * Sets whether there's a SHOW_WHEN_LOCKED activity on top of the task stack, and optionally,
+     * information about the activity itself.
+     *
+     * If no value is provided for [taskInfo], we'll default to the current [taskInfo].
+     *
+     * The [taskInfo] is always present when this method is called from the occlude/unocclude
+     * animation runners. We use the default when calling from [KeyguardService.isOccluded], since
+     * we only receive a true/false value there. isOccluded is mostly redundant - it's almost always
+     * called with true after an occlusion animation has started, and with false after an unocclude
+     * animation has started. In those cases, we don't want to clear out the taskInfo just because
+     * it wasn't available at that call site.
+     */
+    fun setShowWhenLockedActivityInfo(
+        onTop: Boolean,
+        taskInfo: RunningTaskInfo? = showWhenLockedActivityInfo.value.taskInfo
+    ) {
+        showWhenLockedActivityInfo.value =
+            ShowWhenLockedActivityInfo(
+                isOnTop = onTop,
+                taskInfo = taskInfo,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index ad0c3fb..3a6423d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.Position
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
@@ -79,6 +80,12 @@
     val keyguardAlpha: StateFlow<Float>
 
     /**
+     * Observable of the relative offset of the lock-screen clock from its natural position on the
+     * screen.
+     */
+    val clockPosition: StateFlow<Position>
+
+    /**
      * Observable for whether the keyguard is showing.
      *
      * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
@@ -88,6 +95,7 @@
     val isKeyguardShowing: Flow<Boolean>
 
     /** Is an activity showing over the keyguard? */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED")
     val isKeyguardOccluded: Flow<Boolean>
 
     /**
@@ -233,6 +241,11 @@
     fun setKeyguardAlpha(alpha: Float)
 
     /**
+     * Sets the relative offset of the lock-screen clock from its natural position on the screen.
+     */
+    fun setClockPosition(x: Int, y: Int)
+
+    /**
      * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
      */
     fun isUdfpsSupported(): Boolean
@@ -311,6 +324,9 @@
     private val _keyguardAlpha = MutableStateFlow(1f)
     override val keyguardAlpha = _keyguardAlpha.asStateFlow()
 
+    private val _clockPosition = MutableStateFlow(Position(0, 0))
+    override val clockPosition = _clockPosition.asStateFlow()
+
     private val _clockShouldBeCentered = MutableStateFlow(true)
     override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow()
 
@@ -662,6 +678,10 @@
         _keyguardAlpha.value = alpha
     }
 
+    override fun setClockPosition(x: Int, y: Int) {
+        _clockPosition.value = Position(x, y)
+    }
+
     override fun isUdfpsSupported(): Boolean = keyguardUpdateMonitor.isUdfpsSupported
 
     override fun setQuickSettingsVisible(isVisible: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index 9b3f13d..d9479de 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -75,7 +75,6 @@
     powerInteractor: PowerInteractor,
     private val scrimLogger: ScrimLogger,
 ) : LightRevealScrimRepository {
-
     companion object {
         val TAG = LightRevealScrimRepository::class.simpleName!!
     }
@@ -156,7 +155,11 @@
     override fun startRevealAmountAnimator(reveal: Boolean) {
         if (reveal == willBeOrIsRevealed) return
         willBeOrIsRevealed = reveal
-        if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
+        if (reveal && !revealAmountAnimator.isRunning) {
+            revealAmountAnimator.start()
+        } else {
+            revealAmountAnimator.reverse()
+        }
         scrimLogger.d(TAG, "startRevealAmountAnimator, reveal: ", reveal)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
index ee892a8..7ae70a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
@@ -63,19 +63,18 @@
                 burnInHelperWrapper.burnInProgressOffset()
             )
 
-    /** Given the max x,y dimens, determine the current translation shifts. */
-    fun burnIn(xDimenResourceId: Int, yDimenResourceId: Int): Flow<BurnInModel> {
-        return combine(
-                burnInOffset(xDimenResourceId, isXAxis = true),
-                burnInOffset(yDimenResourceId, isXAxis = false).map {
-                    it * 2 - context.resources.getDimensionPixelSize(yDimenResourceId)
+    val keyguardBurnIn: Flow<BurnInModel> =
+        combine(
+                burnInOffset(R.dimen.burn_in_prevention_offset_x, isXAxis = true),
+                burnInOffset(R.dimen.burn_in_prevention_offset_y, isXAxis = false).map {
+                    it * 2 -
+                        context.resources.getDimensionPixelSize(R.dimen.burn_in_prevention_offset_y)
                 }
             ) { translationX, translationY ->
                 BurnInModel(translationX, translationY, burnInHelperWrapper.burnInScale())
             }
             .distinctUntilChanged()
             .stateIn(scope, SharingStarted.Lazily, BurnInModel())
-    }
 
     /**
      * Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index 6aed944..88ddfd4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -46,13 +47,16 @@
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val communalInteractor: CommunalInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.ALTERNATE_BOUNCER,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
@@ -112,6 +116,11 @@
     }
 
     private fun listenForAlternateBouncerToGone() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // Handled via #dismissAlternateBouncer.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
                 .sampleUtil(finishedKeyguardState, ::Pair)
@@ -149,6 +158,10 @@
         }
     }
 
+    fun dismissAlternateBouncer() {
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
+    }
+
     companion object {
         const val TAG = "FromAlternateBouncerTransitionInteractor"
         val TRANSITION_DURATION_MS = 300.milliseconds
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index dbd5e26..9040e03 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -27,14 +27,17 @@
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
 import com.android.systemui.util.kotlin.sample
+import java.util.UUID
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -47,29 +50,118 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.AOD,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
-        listenForAodToLockscreen()
+        listenForAodToAwake()
+        listenForAodToOccluded()
         listenForAodToPrimaryBouncer()
         listenForAodToGone()
-        listenForAodToOccluded()
         listenForTransitionToCamera(scope, keyguardInteractor)
     }
 
     /**
+     * Listen for the signal that we're waking up and figure what state we need to transition to.
+     */
+    private fun listenForAodToAwake() {
+        val transitionToLockscreen: suspend (TransitionStep) -> UUID? =
+            { startedStep: TransitionStep ->
+                val modeOnCanceled =
+                    if (startedStep.from == KeyguardState.LOCKSCREEN) {
+                        TransitionModeOnCanceled.REVERSE
+                    } else if (startedStep.from == KeyguardState.GONE) {
+                        TransitionModeOnCanceled.RESET
+                    } else {
+                        TransitionModeOnCanceled.LAST_VALUE
+                    }
+                startTransitionTo(
+                    toState = KeyguardState.LOCKSCREEN,
+                    modeOnCanceled = modeOnCanceled,
+                )
+            }
+
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // The refactor uses PowerInteractor's wakefulness, which is the earliest wake signal
+            // available. We have all of the information we need at this time to make a decision
+            // about where to transition.
+            scope.launch {
+                powerInteractor.detailedWakefulness
+                    // React only to wake events.
+                    .filter { it.isAwake() }
+                    .sample(
+                        startedKeyguardTransitionStep,
+                        keyguardInteractor.biometricUnlockState,
+                        keyguardInteractor.primaryBouncerShowing,
+                    )
+                    // Make sure we've at least STARTED a transition to AOD.
+                    .filter { (_, startedStep, _, _) -> startedStep.to == KeyguardState.AOD }
+                    .collect { (_, startedStep, biometricUnlockState, primaryBouncerShowing) ->
+                        // Check with the superclass to see if an occlusion transition is needed.
+                        // Also, don't react to wake and unlock events, as we'll be receiving a call
+                        // to #dismissAod() shortly when the authentication completes.
+                        if (
+                            !maybeStartTransitionToOccludedOrInsecureCamera() &&
+                                !isWakeAndUnlock(biometricUnlockState) &&
+                                !primaryBouncerShowing
+                        ) {
+                            transitionToLockscreen(startedStep)
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor
+                    .dozeTransitionTo(DozeStateModel.FINISH)
+                    .sample(
+                        keyguardInteractor.isKeyguardShowing,
+                        startedKeyguardTransitionStep,
+                        keyguardInteractor.isKeyguardOccluded,
+                        keyguardInteractor.biometricUnlockState,
+                        keyguardInteractor.primaryBouncerShowing,
+                    )
+                    .collect {
+                        (
+                            _,
+                            isKeyguardShowing,
+                            lastStartedStep,
+                            occluded,
+                            biometricUnlockState,
+                            primaryBouncerShowing) ->
+                        if (
+                            lastStartedStep.to == KeyguardState.AOD &&
+                                !occluded &&
+                                !isWakeAndUnlock(biometricUnlockState) &&
+                                isKeyguardShowing &&
+                                !primaryBouncerShowing
+                        ) {
+                            transitionToLockscreen(lastStartedStep)
+                        }
+                    }
+            }
+        }
+    }
+
+    /**
      * There are cases where the transition to AOD begins but never completes, such as tapping power
      * during an incoming phone call when unlocked. In this case, GONE->AOD should be interrupted to
      * run AOD->OCCLUDED.
      */
     private fun listenForAodToOccluded() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // Handled by calls to maybeStartTransitionToOccludedOrInsecureCamera on waking.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardOccluded
                 .sample(startedKeyguardTransitionStep, ::Pair)
@@ -84,49 +176,6 @@
         }
     }
 
-    private fun listenForAodToLockscreen() {
-        scope.launch {
-            keyguardInteractor
-                .dozeTransitionTo(DozeStateModel.FINISH)
-                .sample(
-                    keyguardInteractor.isKeyguardShowing,
-                    startedKeyguardTransitionStep,
-                    keyguardInteractor.isKeyguardOccluded,
-                    keyguardInteractor.biometricUnlockState,
-                    keyguardInteractor.primaryBouncerShowing,
-                )
-                .collect {
-                    (
-                        _,
-                        isKeyguardShowing,
-                        lastStartedStep,
-                        occluded,
-                        biometricUnlockState,
-                        primaryBouncerShowing) ->
-                    if (
-                        lastStartedStep.to == KeyguardState.AOD &&
-                            !occluded &&
-                            !isWakeAndUnlock(biometricUnlockState) &&
-                            isKeyguardShowing &&
-                            !primaryBouncerShowing
-                    ) {
-                        val modeOnCanceled =
-                            if (lastStartedStep.from == KeyguardState.LOCKSCREEN) {
-                                TransitionModeOnCanceled.REVERSE
-                            } else if (lastStartedStep.from == KeyguardState.GONE) {
-                                TransitionModeOnCanceled.RESET
-                            } else {
-                                TransitionModeOnCanceled.LAST_VALUE
-                            }
-                        startTransitionTo(
-                            toState = KeyguardState.LOCKSCREEN,
-                            modeOnCanceled = modeOnCanceled,
-                        )
-                    }
-                }
-        }
-    }
-
     /**
      * If there is a biometric lockout and FPS is tapped while on AOD, it should go directly to the
      * PRIMARY_BOUNCER.
@@ -145,6 +194,7 @@
 
     private fun listenForAodToGone() {
         if (KeyguardWmStateRefactor.isEnabled) {
+            // Handled via #dismissAod.
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 8591fe7..57b2a63 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -34,6 +35,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -46,18 +48,22 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForDozingToAny()
+        listenForWakeFromDozing()
         listenForTransitionToCamera(scope, keyguardInteractor)
     }
 
@@ -70,6 +76,10 @@
         }
 
     private fun listenForDozingToAny() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
         scope.launch {
             powerInteractor.isAwake
                 .debounce(50L)
@@ -112,6 +122,58 @@
         }
     }
 
+    /** Figure out what state to transition to when we awake from DOZING. */
+    private fun listenForWakeFromDozing() {
+        if (!KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
+        scope.launch {
+            powerInteractor.detailedWakefulness
+                .filter { it.isAwake() }
+                .sample(
+                    startedKeyguardTransitionStep,
+                    communalInteractor.isIdleOnCommunal,
+                    keyguardInteractor.biometricUnlockState,
+                    canDismissLockScreen,
+                    keyguardInteractor.primaryBouncerShowing,
+                )
+                // If we haven't at least STARTED a transition to DOZING, ignore.
+                .filter { (_, startedStep, _, _) -> startedStep.to == KeyguardState.DOZING }
+                .collect {
+                    (
+                        _,
+                        _,
+                        isIdleOnCommunal,
+                        biometricUnlockState,
+                        canDismissLockscreen,
+                        primaryBouncerShowing) ->
+                    if (
+                        !maybeStartTransitionToOccludedOrInsecureCamera() &&
+                            // Handled by dismissFromDozing().
+                            !isWakeAndUnlock(biometricUnlockState)
+                    ) {
+                        startTransitionTo(
+                            if (canDismissLockscreen) {
+                                KeyguardState.GONE
+                            } else if (primaryBouncerShowing) {
+                                KeyguardState.PRIMARY_BOUNCER
+                            } else if (isIdleOnCommunal) {
+                                KeyguardState.GLANCEABLE_HUB
+                            } else {
+                                KeyguardState.LOCKSCREEN
+                            }
+                        )
+                    }
+                }
+        }
+    }
+
+    /** Dismisses keyguard from the DOZING state. */
+    fun dismissFromDozing() {
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
+    }
+
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
index a6cdaa8..6433d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -46,12 +47,16 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index acfa107..3137138 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -23,10 +23,14 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -34,6 +38,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -47,17 +52,23 @@
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val glanceableHubTransitions: GlanceableHubTransitions,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForDreamingToOccluded()
-        listenForDreamingToGone()
+        listenForDreamingToGoneWhenDismissable()
+        listenForDreamingToGoneFromBiometricUnlock()
+        listenForDreamingToLockscreen()
         listenForDreamingToAodOrDozing()
         listenForTransitionToCamera(scope, keyguardInteractor)
         listenForDreamingToGlanceableHub()
@@ -76,6 +87,7 @@
 
     fun startToLockscreenTransition() {
         scope.launch {
+            KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode()
             if (
                 transitionInteractor.startedKeyguardState.replayCache.last() ==
                     KeyguardState.DREAMING
@@ -86,22 +98,80 @@
     }
 
     private fun listenForDreamingToOccluded() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                combine(
+                        keyguardInteractor.isDreaming,
+                        keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop,
+                        ::Pair
+                    )
+                    .sample(startedKeyguardTransitionStep, ::toTriple)
+                    .filter { (isDreaming, _, startedStep) ->
+                        !isDreaming && startedStep.to == KeyguardState.DREAMING
+                    }
+                    .collect { maybeStartTransitionToOccludedOrInsecureCamera() }
+            }
+        } else {
+            scope.launch {
+                combine(
+                        keyguardInteractor.isKeyguardOccluded,
+                        keyguardInteractor.isDreaming,
+                        ::Pair
+                    )
+                    .sample(startedKeyguardTransitionStep, Utils.Companion::toTriple)
+                    .collect { (isOccluded, isDreaming, lastStartedTransition) ->
+                        if (
+                            isOccluded &&
+                                !isDreaming &&
+                                lastStartedTransition.to == KeyguardState.DREAMING
+                        ) {
+                            startTransitionTo(KeyguardState.OCCLUDED)
+                        }
+                    }
+            }
+        }
+    }
+
+    private fun listenForDreamingToLockscreen() {
+        if (!KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
         scope.launch {
-            combine(keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isDreaming, ::Pair)
-                .sample(startedKeyguardTransitionStep, ::toTriple)
-                .collect { (isOccluded, isDreaming, lastStartedTransition) ->
-                    if (
-                        isOccluded &&
-                            !isDreaming &&
-                            lastStartedTransition.to == KeyguardState.DREAMING
-                    ) {
-                        startTransitionTo(KeyguardState.OCCLUDED)
+            keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
+                .filter { onTop -> !onTop }
+                .sample(startedKeyguardState)
+                .collect { startedState ->
+                    if (startedState == KeyguardState.DREAMING) {
+                        startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
                 }
         }
     }
 
-    private fun listenForDreamingToGone() {
+    private fun listenForDreamingToGoneWhenDismissable() {
+        scope.launch {
+            keyguardInteractor.isAbleToDream
+                .sampleCombine(
+                    keyguardInteractor.isKeyguardShowing,
+                    keyguardInteractor.isKeyguardDismissible,
+                    startedKeyguardTransitionStep,
+                )
+                .collect {
+                    (isDreaming, isKeyguardShowing, isKeyguardDismissible, lastStartedTransition) ->
+                    if (
+                        !isDreaming &&
+                            lastStartedTransition.to == KeyguardState.DREAMING &&
+                            isKeyguardDismissible &&
+                            !isKeyguardShowing
+                    ) {
+                        startTransitionTo(KeyguardState.GONE)
+                    }
+                }
+        }
+    }
+
+    private fun listenForDreamingToGoneFromBiometricUnlock() {
         scope.launch {
             keyguardInteractor.biometricUnlockState
                 .sample(startedKeyguardTransitionStep, ::Pair)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 786c3c6..51bc3ae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -35,6 +36,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -49,14 +51,18 @@
     private val keyguardInteractor: KeyguardInteractor,
     override val transitionRepository: KeyguardTransitionRepository,
     transitionInteractor: KeyguardTransitionInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.GLANCEABLE_HUB,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
+
     override fun start() {
         if (!Flags.communalHub()) {
             return
@@ -151,14 +157,27 @@
     }
 
     private fun listenForHubToOccluded() {
-        scope.launch {
-            and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
-                .sample(startedKeyguardState, ::Pair)
-                .collect { (isOccludedAndNotDreaming, keyguardState) ->
-                    if (isOccludedAndNotDreaming && keyguardState == fromState) {
-                        startTransitionTo(KeyguardState.OCCLUDED)
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
+                    .filter { onTop -> onTop }
+                    .sample(startedKeyguardState)
+                    .collect {
+                        if (it == KeyguardState.GLANCEABLE_HUB) {
+                            maybeStartTransitionToOccludedOrInsecureCamera()
+                        }
                     }
-                }
+            }
+        } else {
+            scope.launch {
+                and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
+                    .sample(startedKeyguardState, ::Pair)
+                    .collect { (isOccludedAndNotDreaming, keyguardState) ->
+                        if (isOccludedAndNotDreaming && keyguardState == fromState) {
+                            startTransitionTo(KeyguardState.OCCLUDED)
+                        }
+                    }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 7593ac2..d5a9bd1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -34,6 +36,8 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -46,14 +50,18 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    private val biometricSettingsRepository: BiometricSettingsRepository,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.GONE,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
@@ -65,23 +73,46 @@
 
     // Primarily for when the user chooses to lock down the device
     private fun listenForGoneToLockscreenOrHub() {
-        scope.launch {
-            keyguardInteractor.isKeyguardShowing
-                .sample(
-                    startedKeyguardState,
-                    communalInteractor.isIdleOnCommunal,
-                )
-                .collect { (isKeyguardShowing, startedState, isIdleOnCommunal) ->
-                    if (isKeyguardShowing && startedState == KeyguardState.GONE) {
-                        val to =
-                            if (isIdleOnCommunal) {
-                                KeyguardState.GLANCEABLE_HUB
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        startTransitionTo(to)
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                biometricSettingsRepository.isCurrentUserInLockdown
+                    .distinctUntilChanged()
+                    .filter { inLockdown -> inLockdown }
+                    .sample(
+                        startedKeyguardState,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (_, startedState, isIdleOnCommunal) ->
+                        if (startedState == KeyguardState.GONE) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to, ownerReason = "User initiated lockdown")
+                        }
                     }
-                }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.isKeyguardShowing
+                    .sample(
+                        startedKeyguardState,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (isKeyguardShowing, startedState, isIdleOnCommunal) ->
+                        if (isKeyguardShowing && startedState == KeyguardState.GONE) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to)
+                        }
+                    }
+            }
         }
     }
 
@@ -122,24 +153,10 @@
 
     private fun listenForGoneToAodOrDozing() {
         scope.launch {
-            powerInteractor.isAsleep
-                .sample(
-                    combine(
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (lastStartedStep.to == KeyguardState.GONE && isAsleep) {
-                        startTransitionTo(
-                            toState =
-                                if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING,
-                            modeOnCanceled = TransitionModeOnCanceled.RESET,
-                        )
-                    }
-                }
+            listenForSleepTransition(
+                from = KeyguardState.GONE,
+                modeOnCanceledFromStartedStep = { TransitionModeOnCanceled.RESET },
+            )
         }
     }
 
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 cb1571e..12b27eb 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
@@ -33,7 +33,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.data.repository.ShadeRepository
 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 java.util.UUID
 import javax.inject.Inject
@@ -44,6 +43,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
@@ -62,21 +62,24 @@
     private val keyguardInteractor: KeyguardInteractor,
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val glanceableHubTransitions: GlanceableHubTransitions,
     private val swipeToDismissInteractor: SwipeToDismissInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.LOCKSCREEN,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForLockscreenToGone()
         listenForLockscreenToGoneDragging()
-        listenForLockscreenToOccluded()
+        listenForLockscreenToOccludedOrDreaming()
         listenForLockscreenToAodOrDozing()
         listenForLockscreenToPrimaryBouncer()
         listenForLockscreenToDreaming()
@@ -115,6 +118,10 @@
     }
 
     private fun listenForLockscreenToDreaming() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
         val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
         scope.launch {
             keyguardInteractor.isAbleToDream
@@ -157,7 +164,10 @@
                     if (
                         isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
                     ) {
-                        startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
+                        startTransitionTo(
+                            KeyguardState.PRIMARY_BOUNCER,
+                            ownerReason = "#listenForLockscreenToPrimaryBouncer"
+                        )
                     }
                 }
         }
@@ -254,7 +264,8 @@
                                 transitionId =
                                     startTransitionTo(
                                         toState = KeyguardState.PRIMARY_BOUNCER,
-                                        animator = null, // transition will be manually controlled
+                                        animator = null, // transition will be manually controlled,
+                                        ownerReason = "#listenForLockscreenToPrimaryBouncerDragging"
                                     )
                             }
                         }
@@ -309,47 +320,52 @@
         }
     }
 
-    private fun listenForLockscreenToOccluded() {
-        scope.launch {
-            keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
-                (isOccluded, keyguardState) ->
-                if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) {
-                    startTransitionTo(KeyguardState.OCCLUDED)
-                }
+    private fun listenForLockscreenToOccludedOrDreaming() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardOcclusionInteractor.showWhenLockedActivityInfo
+                    .filter { it.isOnTop }
+                    .sample(startedKeyguardState, ::Pair)
+                    .collect { (taskInfo, startedState) ->
+                        if (startedState == KeyguardState.LOCKSCREEN) {
+                            startTransitionTo(
+                                if (taskInfo.isDream()) {
+                                    KeyguardState.DREAMING
+                                } else {
+                                    KeyguardState.OCCLUDED
+                                }
+                            )
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.isKeyguardOccluded
+                    .sample(startedKeyguardState, ::Pair)
+                    .collect { (isOccluded, keyguardState) ->
+                        if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) {
+                            startTransitionTo(KeyguardState.OCCLUDED)
+                        }
+                    }
             }
         }
     }
 
     private fun listenForLockscreenToAodOrDozing() {
         scope.launch {
-            powerInteractor.isAsleep
-                .sample(
-                    combine(
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (lastStartedStep.to == KeyguardState.LOCKSCREEN && isAsleep) {
-                        val toState =
-                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
-                        val modeOnCanceled =
-                            if (
-                                toState == KeyguardState.AOD &&
-                                    lastStartedStep.from == KeyguardState.AOD
-                            ) {
-                                TransitionModeOnCanceled.REVERSE
-                            } else {
-                                TransitionModeOnCanceled.LAST_VALUE
-                            }
-                        startTransitionTo(
-                            toState = toState,
-                            modeOnCanceled = modeOnCanceled,
-                        )
+            listenForSleepTransition(
+                from = KeyguardState.LOCKSCREEN,
+                modeOnCanceledFromStartedStep = { startedStep ->
+                    if (
+                        transitionInteractor.asleepKeyguardState.value == KeyguardState.AOD &&
+                            startedStep.from == KeyguardState.AOD
+                    ) {
+                        TransitionModeOnCanceled.REVERSE
+                    } else {
+                        TransitionModeOnCanceled.LAST_VALUE
                     }
                 }
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index efb604d..f10327e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -22,17 +22,17 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
-import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -45,20 +45,23 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.OCCLUDED,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForOccludedToLockscreenOrHub()
         listenForOccludedToDreaming()
-        listenForOccludedToAodOrDozing()
+        listenForOccludedToAsleep()
         listenForOccludedToGone()
         listenForOccludedToAlternateBouncer()
         listenForOccludedToPrimaryBouncer()
@@ -90,43 +93,72 @@
     }
 
     private fun listenForOccludedToLockscreenOrHub() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
+                    .filter { onTop -> !onTop }
+                    .sample(
+                        startedKeyguardState,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (_, startedState, isIdleOnCommunal) ->
+                        // Occlusion signals come from the framework, and should interrupt any
+                        // existing transition
+                        if (startedState == KeyguardState.OCCLUDED) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to)
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.isKeyguardOccluded
+                    .sample(
+                        keyguardInteractor.isKeyguardShowing,
+                        startedKeyguardTransitionStep,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal)
+                        ->
+                        // Occlusion signals come from the framework, and should interrupt any
+                        // existing transition
+                        if (
+                            !isOccluded &&
+                                isShowing &&
+                                lastStartedKeyguardState.to == KeyguardState.OCCLUDED
+                        ) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to)
+                        }
+                    }
+            }
+        }
+    }
+
+    private fun listenForOccludedToGone() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // We don't think OCCLUDED to GONE is possible. You should always have to go via a
+            // *_BOUNCER state to end up GONE. Launching an activity over a dismissable keyguard
+            // dismisses it, and even "extend unlock" doesn't unlock the device in the background.
+            // If we're wrong - sorry, add it back here.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardOccluded
                 .sample(
                     keyguardInteractor.isKeyguardShowing,
                     startedKeyguardTransitionStep,
-                    communalInteractor.isIdleOnCommunal,
-                )
-                .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal) ->
-                    // Occlusion signals come from the framework, and should interrupt any
-                    // existing transition
-                    if (
-                        !isOccluded &&
-                            isShowing &&
-                            lastStartedKeyguardState.to == KeyguardState.OCCLUDED
-                    ) {
-                        val to =
-                            if (isIdleOnCommunal) {
-                                KeyguardState.GLANCEABLE_HUB
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        startTransitionTo(to)
-                    }
-                }
-        }
-    }
-
-    private fun listenForOccludedToGone() {
-        scope.launch {
-            keyguardInteractor.isKeyguardOccluded
-                .sample(
-                    combine(
-                        keyguardInteractor.isKeyguardShowing,
-                        startedKeyguardTransitionStep,
-                        ::Pair
-                    ),
-                    ::toTriple
                 )
                 .collect { (isOccluded, isShowing, lastStartedKeyguardState) ->
                     // Occlusion signals come from the framework, and should interrupt any
@@ -142,25 +174,12 @@
         }
     }
 
-    private fun listenForOccludedToAodOrDozing() {
-        scope.launch {
-            powerInteractor.isAsleep
-                .sample(
-                    combine(
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (lastStartedStep.to == KeyguardState.OCCLUDED && isAsleep) {
-                        startTransitionTo(
-                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
-                        )
-                    }
-                }
-        }
+    fun dismissToGone() {
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
+    }
+
+    private fun listenForOccludedToAsleep() {
+        scope.launch { listenForSleepTransition(from = KeyguardState.OCCLUDED) }
     }
 
     private fun listenForOccludedToAlternateBouncer() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index c5a2846..391dccc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
-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 com.android.wm.shell.animation.Interpolators
@@ -42,6 +41,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
 
@@ -59,18 +59,21 @@
     private val flags: FeatureFlags,
     private val keyguardSecurityModel: KeyguardSecurityModel,
     private val selectedUserInteractor: SelectedUserInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.PRIMARY_BOUNCER,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForPrimaryBouncerToGone()
-        listenForPrimaryBouncerToAodOrDozing()
+        listenForPrimaryBouncerToAsleep()
         listenForPrimaryBouncerToLockscreenHubOrOccluded()
         listenForPrimaryBouncerToDreamingLockscreenHosted()
         listenForTransitionToCamera(scope, keyguardInteractor)
@@ -128,72 +131,86 @@
     }
 
     private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() {
-        scope.launch {
-            keyguardInteractor.primaryBouncerShowing
-                .sample(
-                    powerInteractor.isAwake,
-                    startedKeyguardTransitionStep,
-                    keyguardInteractor.isKeyguardOccluded,
-                    keyguardInteractor.isDreaming,
-                    keyguardInteractor.isActiveDreamLockscreenHosted,
-                    communalInteractor.isIdleOnCommunal,
-                )
-                .collect {
-                    (
-                        isBouncerShowing,
-                        isAwake,
-                        lastStartedTransitionStep,
-                        occluded,
-                        isDreaming,
-                        isActiveDreamLockscreenHosted,
-                        isIdleOnCommunal) ->
-                    if (
-                        !isBouncerShowing &&
-                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
-                            isAwake &&
-                            !isActiveDreamLockscreenHosted
-                    ) {
-                        val toState =
-                            if (occluded && !isDreaming) {
-                                KeyguardState.OCCLUDED
-                            } else if (isIdleOnCommunal) {
-                                KeyguardState.GLANCEABLE_HUB
-                            } else if (isDreaming) {
-                                KeyguardState.DREAMING
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        startTransitionTo(toState)
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardInteractor.primaryBouncerShowing
+                    .sample(
+                        startedKeyguardTransitionStep,
+                        powerInteractor.isAwake,
+                        keyguardInteractor.isActiveDreamLockscreenHosted,
+                        communalInteractor.isIdleOnCommunal
+                    )
+                    .filter { (_, startedStep, _, _) ->
+                        startedStep.to == KeyguardState.PRIMARY_BOUNCER
                     }
-                }
+                    .collect {
+                        (
+                            isBouncerShowing,
+                            _,
+                            isAwake,
+                            isActiveDreamLockscreenHosted,
+                            isIdleOnCommunal) ->
+                        if (
+                            !maybeStartTransitionToOccludedOrInsecureCamera() &&
+                                !isBouncerShowing &&
+                                isAwake &&
+                                !isActiveDreamLockscreenHosted
+                        ) {
+                            val toState =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(toState)
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.primaryBouncerShowing
+                    .sample(
+                        powerInteractor.isAwake,
+                        startedKeyguardTransitionStep,
+                        keyguardInteractor.isKeyguardOccluded,
+                        keyguardInteractor.isDreaming,
+                        keyguardInteractor.isActiveDreamLockscreenHosted,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect {
+                        (
+                            isBouncerShowing,
+                            isAwake,
+                            lastStartedTransitionStep,
+                            occluded,
+                            isDreaming,
+                            isActiveDreamLockscreenHosted,
+                            isIdleOnCommunal) ->
+                        if (
+                            !isBouncerShowing &&
+                                lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
+                                isAwake &&
+                                !isActiveDreamLockscreenHosted
+                        ) {
+                            val toState =
+                                if (occluded && !isDreaming) {
+                                    KeyguardState.OCCLUDED
+                                } else if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else if (isDreaming) {
+                                    KeyguardState.DREAMING
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(toState)
+                        }
+                    }
+            }
         }
     }
 
-    private fun listenForPrimaryBouncerToAodOrDozing() {
-        scope.launch {
-            keyguardInteractor.primaryBouncerShowing
-                .sample(
-                    combine(
-                        powerInteractor.isAsleep,
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Triple
-                    ),
-                    ::toQuad
-                )
-                .collect { (isBouncerShowing, isAsleep, lastStartedTransitionStep, isAodAvailable)
-                    ->
-                    if (
-                        !isBouncerShowing &&
-                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
-                            isAsleep
-                    ) {
-                        startTransitionTo(
-                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
-                        )
-                    }
-                }
-        }
+    private fun listenForPrimaryBouncerToAsleep() {
+        scope.launch { listenForSleepTransition(from = KeyguardState.PRIMARY_BOUNCER) }
     }
 
     private fun listenForPrimaryBouncerToDreamingLockscreenHosted() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
index e5bb5a0..d2a7486 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
@@ -22,8 +22,6 @@
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 
 /** Encapsulates business-logic specifically related to the keyguard bottom area. */
 @SysUISingleton
@@ -37,12 +35,10 @@
     /** The amount of alpha for the UI components of the bottom area. */
     val alpha: Flow<Float> = repository.bottomAreaAlpha
     /** The position of the keyguard clock. */
-    private val _clockPosition = MutableStateFlow(Position(0, 0))
-    @Deprecated("with migrateClocksToBlueprint()")
-    val clockPosition: Flow<Position> = _clockPosition.asStateFlow()
+    val clockPosition: Flow<Position> = repository.clockPosition
 
     fun setClockPosition(x: Int, y: Int) {
-        _clockPosition.value = Position(x, y)
+        repository.setClockPosition(x, y)
     }
 
     fun setAlpha(alpha: Float) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index fbf1e22..f321bd7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.common.shared.model.NotificationContainerBounds
+import com.android.systemui.common.shared.model.Position
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -162,15 +163,18 @@
             .distinctUntilChanged()
 
     /** Whether the keyguard is showing or not. */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState")
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
 
     /** Whether the keyguard is dismissible or not. */
     val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible
 
     /** Whether the keyguard is occluded (covered by an activity). */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED")
     val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
 
     /** Whether the keyguard is going away. */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.GONE")
     val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
 
     /** Keyguard can be clipped at the top as the shade is dragged */
@@ -231,6 +235,9 @@
     /** The approximate location on the screen of the face unlock sensor, if one is available. */
     val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation
 
+    /** The position of the keyguard clock. */
+    val clockPosition: Flow<Position> = repository.clockPosition
+
     @Deprecated("Use the relevant TransitionViewModel")
     val keyguardAlpha: Flow<Float> = repository.keyguardAlpha
 
@@ -338,6 +345,10 @@
         repository.setQuickSettingsVisible(isVisible)
     }
 
+    fun setClockPosition(x: Int, y: Int) {
+        repository.setClockPosition(x, y)
+    }
+
     fun setAlpha(alpha: Float) {
         repository.setKeyguardAlpha(alpha)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
new file mode 100644
index 0000000..9aa2202
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.app.ActivityManager.RunningTaskInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardOcclusionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Logic related to keyguard occlusion. The keyguard is occluded when an activity with
+ * FLAG_SHOW_WHEN_LOCKED is on top of the activity task stack, with that activity displaying on top
+ * of ("occluding") the lockscreen UI. Common examples of this are Google Maps Navigation and the
+ * secure camera.
+ *
+ * This should usually be used only by keyguard internal classes. Most System UI use cases should
+ * use [KeyguardTransitionInteractor] to see if we're in [KeyguardState.OCCLUDED] instead.
+ */
+@SysUISingleton
+class KeyguardOcclusionInteractor
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    val repository: KeyguardOcclusionRepository,
+    val powerInteractor: PowerInteractor,
+    val transitionInteractor: KeyguardTransitionInteractor,
+    val keyguardInteractor: KeyguardInteractor,
+) {
+    val showWhenLockedActivityInfo = repository.showWhenLockedActivityInfo.asStateFlow()
+
+    /**
+     * Whether a SHOW_WHEN_LOCKED activity is on top of the task stack. This does not necessarily
+     * mean we're OCCLUDED, as we could be GONE (unlocked), with an activity that can (but is not
+     * currently) displaying over the lockscreen.
+     *
+     * Transition interactors use this to determine when we should transition to the OCCLUDED state.
+     *
+     * Outside of the transition/occlusion interactors, you almost certainly don't want to use this.
+     * Instead, use KeyguardTransitionInteractor to figure out if we're in KeyguardState.OCCLUDED.
+     */
+    val isShowWhenLockedActivityOnTop = showWhenLockedActivityInfo.map { it.isOnTop }
+
+    /** Whether we should start a transition due to the power button launch gesture. */
+    fun shouldTransitionFromPowerButtonGesture(): Boolean {
+        // powerButtonLaunchGestureTriggered remains true while we're awake from a power button
+        // gesture. Check that we were asleep or transitioning to asleep before starting a
+        // transition, to ensure we don't transition while moving between, for example,
+        // *_BOUNCER -> LOCKSCREEN.
+        return powerInteractor.detailedWakefulness.value.powerButtonLaunchGestureTriggered &&
+            KeyguardState.deviceIsAsleepInState(transitionInteractor.getStartedState())
+    }
+
+    /**
+     * Whether the SHOW_WHEN_LOCKED activity was launched from the double tap power button gesture.
+     * This remains true while the activity is running and emits false once it is killed.
+     */
+    val showWhenLockedActivityLaunchedFromPowerGesture =
+        merge(
+                // Emit true when the power launch gesture is triggered, since this means a
+                // SHOW_WHEN_LOCKED activity will be launched from the gesture (unless we're
+                // currently
+                // GONE, in which case we're going back to GONE and launching the insecure camera).
+                powerInteractor.detailedWakefulness
+                    .sample(transitionInteractor.currentKeyguardState, ::Pair)
+                    .map { (wakefulness, currentKeyguardState) ->
+                        wakefulness.powerButtonLaunchGestureTriggered &&
+                            currentKeyguardState != KeyguardState.GONE
+                    },
+                // Emit false once that activity goes away.
+                isShowWhenLockedActivityOnTop.filter { !it }.map { false }
+            )
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    /**
+     * Whether launching an occluding activity will automatically dismiss keyguard. This happens if
+     * the keyguard is dismissable.
+     */
+    val occludingActivityWillDismissKeyguard =
+        keyguardInteractor.isKeyguardDismissible.stateIn(scope, SharingStarted.Eagerly, false)
+
+    /**
+     * Called to let System UI know that WM says a SHOW_WHEN_LOCKED activity is on top (or no longer
+     * on top).
+     *
+     * This signal arrives from WM when a SHOW_WHEN_LOCKED activity is started or killed - it is
+     * never set directly by System UI. While we might be the reason the activity was started
+     * (launching the camera from the power button gesture), we ultimately only receive this signal
+     * once that activity starts. It's up to us to start the appropriate keyguard transitions,
+     * because that activity is going to be visible (or not) regardless.
+     */
+    fun setWmNotifiedShowWhenLockedActivityOnTop(
+        showWhenLockedActivityOnTop: Boolean,
+        taskInfo: RunningTaskInfo? = null
+    ) {
+        repository.setShowWhenLockedActivityInfo(showWhenLockedActivityOnTop, taskInfo)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index b0a3881..8eb1a50 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -55,6 +56,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -66,6 +68,7 @@
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
+    private val shadeInteractor: ShadeInteractor,
     private val lockPatternUtils: LockPatternUtils,
     private val keyguardStateController: KeyguardStateController,
     private val userTracker: UserTracker,
@@ -100,9 +103,10 @@
             quickAffordanceAlwaysVisible(position),
             keyguardInteractor.isDozing,
             keyguardInteractor.isKeyguardShowing,
+            shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(),
             biometricSettingsRepository.isCurrentUserInLockdown,
-        ) { affordance, isDozing, isKeyguardShowing, isUserInLockdown ->
-            if (!isDozing && isKeyguardShowing && !isUserInLockdown) {
+        ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown ->
+            if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) {
                 affordance
             } else {
                 KeyguardQuickAffordanceModel.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index d81f1f1..c28e49d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -41,6 +41,7 @@
     private val powerInteractor: PowerInteractor,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val shadeInteractor: ShadeInteractor,
+    private val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) {
 
     fun start() {
@@ -91,6 +92,12 @@
         }
 
         scope.launch {
+            keyguardInteractor.isKeyguardDismissible.collect {
+                logger.log(TAG, VERBOSE, "isKeyguardDismissable", it)
+            }
+        }
+
+        scope.launch {
             keyguardInteractor.isAbleToDream.collect {
                 logger.log(TAG, VERBOSE, "isAbleToDream", it)
             }
@@ -125,5 +132,11 @@
                 logger.log(TAG, VERBOSE, "onCameraLaunchDetected", it)
             }
         }
+
+        scope.launch {
+            keyguardOcclusionInteractor.showWhenLockedActivityInfo.collect {
+                logger.log(TAG, VERBOSE, "showWhenLockedActivityInfo", it)
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 37b331c..00902b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
@@ -42,6 +43,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
@@ -56,11 +58,15 @@
 @Inject
 constructor(
     @Application val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
     private val repository: KeyguardTransitionRepository,
     private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>,
     private val fromPrimaryBouncerTransitionInteractor:
         dagger.Lazy<FromPrimaryBouncerTransitionInteractor>,
     private val fromAodTransitionInteractor: dagger.Lazy<FromAodTransitionInteractor>,
+    private val fromAlternateBouncerTransitionInteractor:
+        dagger.Lazy<FromAlternateBouncerTransitionInteractor>,
+    private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>,
 ) {
     private val TAG = this::class.simpleName
 
@@ -207,6 +213,12 @@
             .map { step -> step.to }
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
+    /** Which keyguard state to use when the device goes to sleep. */
+    val asleepKeyguardState: StateFlow<KeyguardState> =
+        keyguardRepository.isAodAvailable
+            .map { aodAvailable -> if (aodAvailable) AOD else DOZING }
+            .stateIn(scope, SharingStarted.Eagerly, DOZING)
+
     /**
      * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
      * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
@@ -368,7 +380,10 @@
         when (val startedState = startedKeyguardState.replayCache.last()) {
             LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
             PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer()
+            ALTERNATE_BOUNCER ->
+                fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer()
             AOD -> fromAodTransitionInteractor.get().dismissAod()
+            DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing()
             else ->
                 Log.e(
                     "KeyguardTransitionInteractor",
@@ -421,12 +436,17 @@
         fromStatePredicate: (KeyguardState) -> Boolean,
         toStatePredicate: (KeyguardState) -> Boolean,
     ): Flow<Boolean> {
+        return isInTransitionWhere { from, to -> fromStatePredicate(from) && toStatePredicate(to) }
+    }
+
+    fun isInTransitionWhere(
+        fromToStatePredicate: (KeyguardState, KeyguardState) -> Boolean
+    ): Flow<Boolean> {
         return repository.transitions
             .filter { it.transitionState != TransitionState.CANCELED }
             .mapLatest {
                 it.transitionState != TransitionState.FINISHED &&
-                    fromStatePredicate(it.from) &&
-                    toStatePredicate(it.to)
+                    fromToStatePredicate(it.from, it.to)
             }
             .distinctUntilChanged()
     }
@@ -447,4 +467,16 @@
      */
     fun isFinishedInStateWhereValue(stateMatcher: (KeyguardState) -> Boolean) =
         stateMatcher(finishedKeyguardState.replayCache.last())
+
+    fun getCurrentState(): KeyguardState {
+        return currentKeyguardState.replayCache.last()
+    }
+
+    fun getStartedState(): KeyguardState {
+        return startedKeyguardState.replayCache.last()
+    }
+
+    fun getFinishedState(): KeyguardState {
+        return finishedKeyguardState.replayCache.last()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 4d731ec..8905c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -43,7 +43,6 @@
     private val scrimLogger: ScrimLogger,
     private val powerInteractor: PowerInteractor,
 ) {
-
     init {
         listenForStartedKeyguardTransitionStep()
     }
@@ -52,9 +51,7 @@
         scope.launch {
             transitionInteractor.startedKeyguardTransitionStep.collect {
                 scrimLogger.d(TAG, "listenForStartedKeyguardTransitionStep", it)
-                lightRevealScrimRepository.startRevealAmountAnimator(
-                    willBeRevealedInState(it.to),
-                )
+                lightRevealScrimRepository.startRevealAmountAnimator(willBeRevealedInState(it.to))
             }
         }
     }
@@ -89,25 +86,25 @@
 
     companion object {
 
-    /**
-     * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given
-     * state after the transition is complete. If false, scrim will be fully hidden.
-     */
-    private fun willBeRevealedInState(state: KeyguardState): Boolean {
-        return when (state) {
-            KeyguardState.OFF -> false
-            KeyguardState.DOZING -> false
-            KeyguardState.AOD -> false
-            KeyguardState.DREAMING -> true
-            KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true
-            KeyguardState.GLANCEABLE_HUB -> true
-            KeyguardState.ALTERNATE_BOUNCER -> true
-            KeyguardState.PRIMARY_BOUNCER -> true
-            KeyguardState.LOCKSCREEN -> true
-            KeyguardState.GONE -> true
-            KeyguardState.OCCLUDED -> true
+        /**
+         * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given
+         * state after the transition is complete. If false, scrim will be fully hidden.
+         */
+        private fun willBeRevealedInState(state: KeyguardState): Boolean {
+            return when (state) {
+                KeyguardState.OFF -> false
+                KeyguardState.DOZING -> false
+                KeyguardState.AOD -> false
+                KeyguardState.DREAMING -> true
+                KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true
+                KeyguardState.GLANCEABLE_HUB -> true
+                KeyguardState.ALTERNATE_BOUNCER -> true
+                KeyguardState.PRIMARY_BOUNCER -> true
+                KeyguardState.LOCKSCREEN -> true
+                KeyguardState.GONE -> true
+                KeyguardState.OCCLUDED -> true
+            }
         }
-    }
 
         val TAG = LightRevealScrimInteractor::class.simpleName!!
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 3ccbdba..375df3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -18,15 +18,20 @@
 
 import android.animation.ValueAnimator
 import android.util.Log
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.sample
 import java.util.UUID
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -46,6 +51,8 @@
     val transitionInteractor: KeyguardTransitionInteractor,
     val mainDispatcher: CoroutineDispatcher,
     val bgDispatcher: CoroutineDispatcher,
+    val powerInteractor: PowerInteractor,
+    val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) {
     val name = this::class.simpleName ?: "UnknownTransitionInteractor"
     abstract val transitionRepository: KeyguardTransitionRepository
@@ -65,7 +72,11 @@
     suspend fun startTransitionTo(
         toState: KeyguardState,
         animator: ValueAnimator? = getDefaultAnimatorForTransitionsToState(toState),
-        modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE
+        modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE,
+        // Even more information about why the owner started this transition, if this is a dangerous
+        // transition (*cough* occlusion) where you'd be sad to not have all the info you can get in
+        // a bugreport.
+        ownerReason: String = "",
     ): UUID? {
         if (
             fromState != transitionInteractor.startedKeyguardState.replayCache.last() &&
@@ -85,7 +96,7 @@
         return withContext(mainDispatcher) {
             transitionRepository.startTransition(
                 TransitionInfo(
-                    name,
+                    name + if (ownerReason.isNotBlank()) "($ownerReason)" else "",
                     fromState,
                     toState,
                     animator,
@@ -95,24 +106,107 @@
         }
     }
 
+    /**
+     * Check whether we need to transition to [KeyguardState.OCCLUDED], based on the presence of a
+     * SHOW_WHEN_LOCKED activity, or back to [KeyguardState.GONE], for some power button launch
+     * gesture cases. If so, start the transition.
+     *
+     * Returns true if a transition was started, false otherwise.
+     */
+    suspend fun maybeStartTransitionToOccludedOrInsecureCamera(): Boolean {
+        if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) {
+            if (transitionInteractor.getCurrentState() == KeyguardState.GONE) {
+                // If the current state is GONE when the launch gesture is triggered, it means we
+                // were in transition from GONE -> DOZING/AOD due to the first power button tap. The
+                // second tap indicates that the user's intent was actually to launch the unlocked
+                // (insecure) camera, so we should transition back to GONE.
+                startTransitionTo(
+                    KeyguardState.GONE,
+                    ownerReason = "Power button gesture while GONE"
+                )
+            } else if (keyguardOcclusionInteractor.occludingActivityWillDismissKeyguard.value) {
+                // The double tap gesture occurred while not GONE (AOD/LOCKSCREEN/etc.), but the
+                // keyguard is dismissable. The activity launch will dismiss the keyguard, so we
+                // should transition to GONE.
+                startTransitionTo(
+                    KeyguardState.GONE,
+                    ownerReason = "Power button gesture on dismissable keyguard"
+                )
+            } else {
+                // Otherwise, the double tap gesture occurred while not GONE and not dismissable,
+                // which means we will launch the secure camera, which OCCLUDES the keyguard.
+                startTransitionTo(
+                    KeyguardState.OCCLUDED,
+                    ownerReason = "Power button gesture on lockscreen"
+                )
+            }
+
+            return true
+        } else if (keyguardOcclusionInteractor.showWhenLockedActivityInfo.value.isOnTop) {
+            // A SHOW_WHEN_LOCKED activity is on top of the task stack. Transition to OCCLUDED so
+            // it's visible.
+            // TODO(b/307976454) - Centralize transition to DREAMING here.
+            startTransitionTo(
+                KeyguardState.OCCLUDED,
+                ownerReason = "SHOW_WHEN_LOCKED activity on top"
+            )
+
+            return true
+        } else {
+            // No transition needed, let the interactor figure out where to go.
+            return false
+        }
+    }
+
+    /**
+     * Transition to the appropriate state when the device goes to sleep while in [from].
+     *
+     * We could also just use [fromState], but it's more readable in the From*TransitionInteractor
+     * if you're explicitly declaring which state you're listening from. If you passed in the wrong
+     * state, [startTransitionTo] would complain anyway.
+     */
+    suspend fun listenForSleepTransition(
+        from: KeyguardState,
+        modeOnCanceledFromStartedStep: (TransitionStep) -> TransitionModeOnCanceled = {
+            TransitionModeOnCanceled.LAST_VALUE
+        }
+    ) {
+        powerInteractor.isAsleep
+            .filter { isAsleep -> isAsleep }
+            .sample(startedKeyguardTransitionStep)
+            .filter { startedStep -> startedStep.to == from }
+            .map(modeOnCanceledFromStartedStep)
+            .collect { modeOnCanceled ->
+                startTransitionTo(
+                    toState = transitionInteractor.asleepKeyguardState.value,
+                    modeOnCanceled = modeOnCanceled,
+                    ownerReason = "Sleep transition triggered"
+                )
+            }
+    }
+
     /** This signal may come in before the occlusion signal, and can provide a custom transition */
     fun listenForTransitionToCamera(
         scope: CoroutineScope,
         keyguardInteractor: KeyguardInteractor,
     ) {
-        scope.launch {
-            keyguardInteractor.onCameraLaunchDetected
-                .sample(transitionInteractor.finishedKeyguardState)
-                .collect { finishedKeyguardState ->
-                    // Other keyguard state transitions may trigger on the first power button push,
-                    // so use the last finishedKeyguardState to determine the overriding FROM state
-                    if (finishedKeyguardState == fromState) {
-                        startTransitionTo(
-                            toState = KeyguardState.OCCLUDED,
-                            modeOnCanceled = TransitionModeOnCanceled.RESET,
-                        )
+        if (!KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardInteractor.onCameraLaunchDetected
+                    .sample(transitionInteractor.finishedKeyguardState)
+                    .collect { finishedKeyguardState ->
+                        // Other keyguard state transitions may trigger on the first power button
+                        // push,
+                        // so use the last finishedKeyguardState to determine the overriding FROM
+                        // state
+                        if (finishedKeyguardState == fromState) {
+                            startTransitionTo(
+                                toState = KeyguardState.OCCLUDED,
+                                modeOnCanceled = TransitionModeOnCanceled.RESET,
+                            )
+                        }
                     }
-                }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index e67a324..98bebd0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -21,8 +21,6 @@
 import android.view.View
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
@@ -39,18 +37,13 @@
     private val clockViewModel: KeyguardClockViewModel,
 ) : KeyguardSection() {
     private lateinit var burnInLayer: AodBurnInLayer
-    private lateinit var emptyView: View
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!migrateClocksToBlueprint()) {
             return
         }
 
         // The burn-in layer requires at least 1 view at all times
-        emptyView =
-            View(context, null).apply {
-                id = R.id.burn_in_layer_empty_view
-                visibility = View.GONE
-            }
+        val emptyView = View(context, null).apply { id = View.generateViewId() }
         constraintLayout.addView(emptyView)
         burnInLayer =
             AodBurnInLayer(context).apply {
@@ -77,13 +70,6 @@
         if (!migrateClocksToBlueprint()) {
             return
         }
-
-        constraintSet.apply {
-            // The empty view should not occupy any space
-            constrainHeight(R.id.burn_in_layer_empty_view, 1)
-            constrainWidth(R.id.burn_in_layer_empty_view, 0)
-            connect(R.id.burn_in_layer_empty_view, BOTTOM, PARENT_ID, BOTTOM)
-        }
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index 62fc1da..7be390a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import android.util.Log
 import android.util.MathUtils
 import com.android.app.animation.Interpolators
 import com.android.keyguard.KeyguardClockSwitch
@@ -63,8 +62,6 @@
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     private val keyguardClockViewModel: KeyguardClockViewModel,
 ) {
-    private val TAG = "AodBurnInViewModel"
-
     /** Horizontal translation for elements that need to apply anti-burn-in tactics. */
     fun translationX(
         params: BurnInParameters,
@@ -74,17 +71,8 @@
 
     /** Vertical translation for elements that need to apply anti-burn-in tactics. */
     fun translationY(
-        burnInParams: BurnInParameters,
+        params: BurnInParameters,
     ): Flow<Float> {
-        val params =
-            if (burnInParams.minViewY < burnInParams.topInset) {
-                // minViewY should never be below the inset. Correct it if needed
-                Log.w(TAG, "minViewY is below topInset: $burnInParams")
-                burnInParams.copy(minViewY = burnInParams.topInset)
-            } else {
-                burnInParams
-            }
-
         return configurationInteractor
             .dimensionPixelSize(R.dimen.keyguard_enter_from_top_translation_y)
             .flatMapLatest { enterFromTopAmount ->
@@ -137,10 +125,7 @@
             keyguardTransitionInteractor.dozeAmountTransition.map {
                 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it.value)
             },
-            burnInInteractor.burnIn(
-                xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
-                yDimenResourceId = R.dimen.burn_in_prevention_offset_y
-            )
+            burnInInteractor.keyguardBurnIn,
         ) { interpolated, burnIn ->
             val useScaleOnly =
                 (clockController(params.clockControllerProvider)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
index a9eec18..7e39a88 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
@@ -43,6 +43,10 @@
             to = KeyguardState.GONE,
         )
 
+    /**
+     * AOD -> GONE should fade out the lockscreen contents. This transition plays both during wake
+     * and unlock, and also during insecure camera launch (which is GONE -> AOD (canceled) -> GONE).
+     */
     fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
         var startAlpha = 1f
         return transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index 105a7ed..445575f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -16,12 +16,15 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
 
 /** Breaks down AOD->OCCLUDED transition into discrete steps for corresponding views to consume. */
 @SysUISingleton
@@ -37,5 +40,23 @@
             to = KeyguardState.OCCLUDED,
         )
 
+    /**
+     * Fade out the lockscreen during a transition to OCCLUDED.
+     *
+     * This happens when pressing the power button while a SHOW_WHEN_LOCKED activity is on the top
+     * of the task stack, as well as when the power button is double tapped on the LOCKSCREEN (the
+     * first tap transitions to AOD, the second cancels that transition and starts AOD -> OCCLUDED.
+     */
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var currentAlpha = 0f
+        return transitionAnimation.sharedFlow(
+            duration = 250.milliseconds,
+            startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
+            onStart = { currentAlpha = viewState.alpha() },
+            onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
+            onCancel = { 0f },
+        )
+    }
+
     override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
new file mode 100644
index 0000000..c0b1195
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.util.MathUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down DOZING->OCCLUDED transition into discrete steps for corresponding views to consume.
+ */
+@SysUISingleton
+class DozingToOccludedTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
+            from = KeyguardState.DOZING,
+            to = KeyguardState.OCCLUDED,
+        )
+
+    /**
+     * Fade out the lockscreen during a transition to OCCLUDED.
+     *
+     * This happens when pressing the power button while a SHOW_WHEN_LOCKED activity is on the top
+     * of the task stack, as well as when the power button is double tapped on the LOCKSCREEN (the
+     * first tap transitions to DOZING, the second cancels that transition and starts DOZING ->
+     * OCCLUDED.
+     */
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var currentAlpha = 0f
+        return transitionAnimation.sharedFlow(
+            duration = 250.milliseconds,
+            startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
+            onStart = { currentAlpha = viewState.alpha() },
+            onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
+            onCancel = { 0f },
+        )
+    }
+
+    override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
+}
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 e35e065..6458eda 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
@@ -17,14 +17,10 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.shared.model.BurnInModel
-import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -36,10 +32,9 @@
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
-    private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
+    bottomAreaInteractor: KeyguardBottomAreaInteractor,
     keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
     private val burnInHelperWrapper: BurnInHelperWrapper,
-    private val burnInInteractor: BurnInInteractor,
     private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
     configurationInteractor: ConfigurationInteractor,
 ) {
@@ -68,37 +63,24 @@
                 }
                 .distinctUntilChanged()
         }
-
-    private val burnIn: Flow<BurnInModel> =
-        burnInInteractor
-            .burnIn(
-                xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
-                yDimenResourceId = R.dimen.default_burn_in_prevention_offset,
-            )
-            .distinctUntilChanged()
-
     /** An observable for the x-offset by which the indication area should be translated. */
     val indicationAreaTranslationX: Flow<Float> =
-        if (migrateClocksToBlueprint() || keyguardBottomAreaRefactor()) {
-            burnIn.map { it.translationX.toFloat() }
+        if (keyguardBottomAreaRefactor()) {
+            keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
         } else {
             bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
         }
 
     /** Returns an observable for the y-offset by which the indication area should be translated. */
     fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
-        return if (migrateClocksToBlueprint()) {
-            burnIn.map { it.translationY.toFloat() }
-        } else {
-            keyguardInteractor.dozeAmount
-                .map { dozeAmount ->
-                    dozeAmount *
-                        (burnInHelperWrapper.burnInOffset(
-                            /* amplitude = */ defaultBurnInOffset * 2,
-                            /* xAxis= */ false,
-                        ) - defaultBurnInOffset)
-                }
-                .distinctUntilChanged()
-        }
+        return keyguardInteractor.dozeAmount
+            .map { dozeAmount ->
+                dozeAmount *
+                    (burnInHelperWrapper.burnInOffset(
+                        /* amplitude = */ defaultBurnInOffset * 2,
+                        /* xAxis= */ false,
+                    ) - defaultBurnInOffset)
+            }
+            .distinctUntilChanged()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 1760b92..f848717 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -73,8 +73,10 @@
         AlternateBouncerToGoneTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
     private val dozingToGoneTransitionViewModel: DozingToGoneTransitionViewModel,
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
+    private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
@@ -170,8 +172,10 @@
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+                        aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToLockscreenTransitionViewModel.lockscreenAlpha,
+                        dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                         dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                         glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
                         goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 840b309..26c63f3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -109,7 +109,7 @@
 import com.android.systemui.media.controls.util.MediaFlags;
 import com.android.systemui.media.controls.util.MediaUiEventLogger;
 import com.android.systemui.media.controls.util.SmallHash;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.monet.Style;
 import com.android.systemui.plugins.ActivityStarter;
@@ -223,7 +223,7 @@
     protected int mUid = Process.INVALID_UID;
     private int mSmartspaceMediaItemsCount;
     private MediaCarouselController mMediaCarouselController;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
     private final FalsingManager mFalsingManager;
     private MetadataAnimationHandler mMetadataAnimationHandler;
     private ColorSchemeTransition mColorSchemeTransition;
@@ -304,7 +304,7 @@
             MediaViewController mediaViewController,
             SeekBarViewModel seekBarViewModel,
             Lazy<MediaDataManager> lazyMediaDataManager,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             MediaCarouselController mediaCarouselController,
             FalsingManager falsingManager,
             SystemClock systemClock,
@@ -324,7 +324,7 @@
         mSeekBarViewModel = seekBarViewModel;
         mMediaViewController = mediaViewController;
         mMediaDataManagerLazy = lazyMediaDataManager;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
         mMediaCarouselController = mediaCarouselController;
         mFalsingManager = falsingManager;
         mSystemClock = systemClock;
@@ -737,7 +737,7 @@
                                     mPackageName, mMediaViewHolder.getSeamlessButton());
                         } else {
                             mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
-                            mMediaOutputDialogFactory.create(mPackageName, true,
+                            mMediaOutputDialogManager.createAndShow(mPackageName, true,
                                     mMediaViewHolder.getSeamlessButton());
                         }
                     } else {
@@ -761,7 +761,7 @@
                                 }
                             }
                         } else {
-                            mMediaOutputDialogFactory.create(mPackageName, true,
+                            mMediaOutputDialogManager.createAndShow(mPackageName, true,
                                     mMediaViewHolder.getSeamlessButton());
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt
index 5d113a9..452cb7e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt
@@ -31,7 +31,8 @@
 ) {
     /** Creates a [LocalMediaManager] for the given package. */
     fun create(packageName: String?): LocalMediaManager {
-        return InfoMediaManager.createInstance(context, packageName, null, localBluetoothManager)
-            .run { LocalMediaManager(context, localBluetoothManager, this, packageName) }
+        return InfoMediaManager.createInstance(context, packageName, localBluetoothManager).run {
+            LocalMediaManager(context, localBluetoothManager, this, packageName)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
deleted file mode 100644
index b6e3937..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media.dialog
-
-import android.app.KeyguardManager
-import android.content.Context
-import android.media.AudioManager
-import android.media.session.MediaSessionManager
-import android.os.PowerExemptionManager
-import android.view.View
-import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import javax.inject.Inject
-
-/**
- * Factory to create [MediaOutputBroadcastDialog] objects.
- */
-class MediaOutputBroadcastDialogFactory @Inject constructor(
-    private val context: Context,
-    private val mediaSessionManager: MediaSessionManager,
-    private val lbm: LocalBluetoothManager?,
-    private val starter: ActivityStarter,
-    private val broadcastSender: BroadcastSender,
-    private val notifCollection: CommonNotifCollection,
-    private val uiEventLogger: UiEventLogger,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val nearbyMediaDevicesManager: NearbyMediaDevicesManager,
-    private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager,
-    private val keyGuardManager: KeyguardManager,
-    private val featureFlags: FeatureFlags,
-    private val userTracker: UserTracker
-) {
-    var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
-
-    /** Creates a [MediaOutputBroadcastDialog] for the given package. */
-    fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
-        // Dismiss the previous dialog, if any.
-        mediaOutputBroadcastDialog?.dismiss()
-
-        val controller = MediaOutputController(context, packageName,
-                mediaSessionManager, lbm, starter, notifCollection,
-                dialogTransitionAnimator, nearbyMediaDevicesManager, audioManager,
-                powerExemptionManager, keyGuardManager, featureFlags, userTracker)
-        val dialog =
-                MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
-        mediaOutputBroadcastDialog = dialog
-
-        // Show the dialog.
-        if (view != null) {
-            dialogTransitionAnimator.showFromView(dialog, view)
-        } else {
-            dialog.show()
-        }
-    }
-
-    /** dismiss [MediaOutputBroadcastDialog] if exist. */
-    fun dismiss() {
-        mediaOutputBroadcastDialog?.dismiss()
-        mediaOutputBroadcastDialog = null
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
new file mode 100644
index 0000000..54d175c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog
+
+import android.content.Context
+import android.view.View
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import javax.inject.Inject
+
+/** Manager to create and show a [MediaOutputBroadcastDialog]. */
+class MediaOutputBroadcastDialogManager
+@Inject
+constructor(
+    private val context: Context,
+    private val broadcastSender: BroadcastSender,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val mediaOutputControllerFactory: MediaOutputController.Factory
+) {
+    var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
+
+    /** Creates a [MediaOutputBroadcastDialog] for the given package. */
+    fun createAndShow(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+        // Dismiss the previous dialog, if any.
+        mediaOutputBroadcastDialog?.dismiss()
+
+        val controller = mediaOutputControllerFactory.create(packageName)
+        val dialog =
+            MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
+        mediaOutputBroadcastDialog = dialog
+
+        // Show the dialog.
+        if (view != null) {
+            dialogTransitionAnimator.showFromView(dialog, view)
+        } else {
+            dialog.show()
+        }
+    }
+
+    /** dismiss [MediaOutputBroadcastDialog] if exist. */
+    fun dismiss() {
+        mediaOutputBroadcastDialog?.dismiss()
+        mediaOutputBroadcastDialog = null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index b3b7bce..adee7f2c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -64,6 +64,7 @@
 import android.view.WindowManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.graphics.drawable.IconCompat;
 
@@ -91,6 +92,10 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -105,8 +110,6 @@
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
-import javax.inject.Inject;
-
 /**
  * Controller for media output dialog
  */
@@ -170,10 +173,13 @@
         ACTION_BROADCAST_INFO_ICON
     }
 
-    @Inject
-    public MediaOutputController(@NonNull Context context, String packageName,
-            MediaSessionManager mediaSessionManager, LocalBluetoothManager
-            lbm, ActivityStarter starter,
+    @AssistedInject
+    public MediaOutputController(
+            Context context,
+            @Assisted String packageName,
+            MediaSessionManager mediaSessionManager,
+            @Nullable LocalBluetoothManager lbm,
+            ActivityStarter starter,
             CommonNotifCollection notifCollection,
             DialogTransitionAnimator dialogTransitionAnimator,
             NearbyMediaDevicesManager nearbyMediaDevicesManager,
@@ -193,7 +199,7 @@
         mKeyGuardManager = keyGuardManager;
         mFeatureFlags = featureFlags;
         mUserTracker = userTracker;
-        InfoMediaManager imm = InfoMediaManager.createInstance(mContext, packageName, null, lbm);
+        InfoMediaManager imm = InfoMediaManager.createInstance(mContext, packageName, lbm);
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
         mDialogTransitionAnimator = dialogTransitionAnimator;
@@ -222,6 +228,12 @@
                 R.dimen.media_output_dialog_selectable_margin_end);
     }
 
+    @AssistedFactory
+    public interface Factory {
+        /** Construct a MediaOutputController */
+        MediaOutputController create(String packageName);
+    }
+
     protected void start(@NonNull Callback cb) {
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
similarity index 62%
rename from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
rename to packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 02be0c1..e7816a4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -16,43 +16,24 @@
 
 package com.android.systemui.media.dialog
 
-import android.app.KeyguardManager
 import android.content.Context
-import android.media.AudioManager
-import android.media.session.MediaSessionManager
-import android.os.PowerExemptionManager
 import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import javax.inject.Inject
 
-/** Factory to create [MediaOutputDialog] objects. */
-open class MediaOutputDialogFactory
+/** Manager to create and show a [MediaOutputDialog]. */
+open class MediaOutputDialogManager
 @Inject
 constructor(
     private val context: Context,
-    private val mediaSessionManager: MediaSessionManager,
-    private val lbm: LocalBluetoothManager?,
-    private val starter: ActivityStarter,
     private val broadcastSender: BroadcastSender,
-    private val notifCollection: CommonNotifCollection,
     private val uiEventLogger: UiEventLogger,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val nearbyMediaDevicesManager: NearbyMediaDevicesManager,
-    private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager,
-    private val keyGuardManager: KeyguardManager,
-    private val featureFlags: FeatureFlags,
-    private val userTracker: UserTracker
+    private val mediaOutputControllerFactory: MediaOutputController.Factory,
 ) {
     companion object {
         const val INTERACTION_JANK_TAG = "media_output"
@@ -60,8 +41,8 @@
     }
 
     /** Creates a [MediaOutputDialog] for the given package. */
-    open fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
-        createWithController(
+    open fun createAndShow(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+        createAndShowWithController(
             packageName,
             aboveStatusBar,
             controller =
@@ -78,12 +59,12 @@
     }
 
     /** Creates a [MediaOutputDialog] for the given package. */
-    open fun createWithController(
+    open fun createAndShowWithController(
         packageName: String,
         aboveStatusBar: Boolean,
         controller: DialogTransitionAnimator.Controller?,
     ) {
-        create(
+        createAndShow(
             packageName,
             aboveStatusBar,
             dialogTransitionAnimatorController = controller,
@@ -91,8 +72,10 @@
         )
     }
 
-    open fun createDialogForSystemRouting(controller: DialogTransitionAnimator.Controller? = null) {
-        create(
+    open fun createAndShowForSystemRouting(
+        controller: DialogTransitionAnimator.Controller? = null
+    ) {
+        createAndShow(
             packageName = null,
             aboveStatusBar = false,
             dialogTransitionAnimatorController = null,
@@ -100,7 +83,7 @@
         )
     }
 
-    private fun create(
+    private fun createAndShow(
         packageName: String?,
         aboveStatusBar: Boolean,
         dialogTransitionAnimatorController: DialogTransitionAnimator.Controller?,
@@ -109,23 +92,9 @@
         // Dismiss the previous dialog, if any.
         mediaOutputDialog?.dismiss()
 
-        val controller =
-            MediaOutputController(
-                context,
-                packageName,
-                mediaSessionManager,
-                lbm,
-                starter,
-                notifCollection,
-                dialogTransitionAnimator,
-                nearbyMediaDevicesManager,
-                audioManager,
-                powerExemptionManager,
-                keyGuardManager,
-                featureFlags,
-                userTracker
-            )
-        val dialog =
+        val controller = mediaOutputControllerFactory.create(packageName)
+
+        val mediaOutputDialog =
             MediaOutputDialog(
                 context,
                 aboveStatusBar,
@@ -135,16 +104,15 @@
                 uiEventLogger,
                 includePlaybackAndAppMetadata
             )
-        mediaOutputDialog = dialog
 
         // Show the dialog.
         if (dialogTransitionAnimatorController != null) {
             dialogTransitionAnimator.show(
-                dialog,
+                mediaOutputDialog,
                 dialogTransitionAnimatorController,
             )
         } else {
-            dialog.show()
+            mediaOutputDialog.show()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
index 38d31ed..774792f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
@@ -31,8 +31,8 @@
  * BroadcastReceiver for handling media output intent
  */
 class MediaOutputDialogReceiver @Inject constructor(
-    private val mediaOutputDialogFactory: MediaOutputDialogFactory,
-    private val mediaOutputBroadcastDialogFactory: MediaOutputBroadcastDialogFactory
+        private val mediaOutputDialogManager: MediaOutputDialogManager,
+        private val mediaOutputBroadcastDialogManager: MediaOutputBroadcastDialogManager
 ) : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
         when (intent.action) {
@@ -42,7 +42,7 @@
                 launchMediaOutputDialogIfPossible(packageName)
             }
             MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG -> {
-                mediaOutputDialogFactory.createDialogForSystemRouting()
+                mediaOutputDialogManager.createAndShowForSystemRouting()
             }
             MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG -> {
                 if (!legacyLeAudioSharing()) return
@@ -55,7 +55,7 @@
 
     private fun launchMediaOutputDialogIfPossible(packageName: String?) {
         if (!packageName.isNullOrEmpty()) {
-            mediaOutputDialogFactory.create(packageName, false)
+            mediaOutputDialogManager.createAndShow(packageName, false)
         } else if (DEBUG) {
             Log.e(TAG, "Unable to launch media output dialog. Package name is empty.")
         }
@@ -63,7 +63,7 @@
 
     private fun launchMediaOutputBroadcastDialogIfPossible(packageName: String?) {
         if (!packageName.isNullOrEmpty()) {
-            mediaOutputBroadcastDialogFactory.create(
+            mediaOutputBroadcastDialogManager.createAndShow(
                     packageName, aboveStatusBar = true, view = null)
         } else if (DEBUG) {
             Log.e(TAG, "Unable to launch media output broadcast dialog. Package name is empty.")
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
index b5b1f0f..6e7e0f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
@@ -34,15 +34,15 @@
     private static final String TAG = "MediaOutputSwitcherDialogUI";
 
     private final CommandQueue mCommandQueue;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
 
     @Inject
     public MediaOutputSwitcherDialogUI(
             Context context,
             CommandQueue commandQueue,
-            MediaOutputDialogFactory mediaOutputDialogFactory) {
+            MediaOutputDialogManager mediaOutputDialogManager) {
         mCommandQueue = commandQueue;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
     }
 
     @Override
@@ -54,7 +54,7 @@
     @MainThread
     public void showMediaOutputSwitcher(String packageName) {
         if (!TextUtils.isEmpty(packageName)) {
-            mMediaOutputDialogFactory.create(packageName, false, null);
+            mMediaOutputDialogManager.createAndShow(packageName, false, null);
         } else {
             Log.e(TAG, "Unable to launch media output dialog. Package name is empty.");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt
deleted file mode 100644
index fc45228..0000000
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt
+++ /dev/null
@@ -1,33 +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.mediaprojection.devicepolicy
-
-import android.content.Context
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.phone.SystemUIDialog
-
-/** Dialog that shows that screen capture is disabled on this device. */
-class ScreenCaptureDisabledDialog(context: Context) : SystemUIDialog(context) {
-
-    init {
-        setTitle(context.getString(R.string.screen_capturing_disabled_by_policy_dialog_title))
-        setMessage(
-            context.getString(R.string.screen_capturing_disabled_by_policy_dialog_description)
-        )
-        setIcon(R.drawable.ic_cast)
-        setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> cancel() }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialogDelegate.kt
new file mode 100644
index 0000000..8aed535
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialogDelegate.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.content.res.Resources
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+
+/** Dialog that shows that screen capture is disabled on this device. */
+class ScreenCaptureDisabledDialogDelegate @Inject constructor(
+        @Main private val resources: Resources,
+        private val systemUIDialogFactory: SystemUIDialog.Factory
+) : SystemUIDialog.Delegate {
+
+    override fun createDialog(): SystemUIDialog {
+        val dialog = systemUIDialogFactory.create(this)
+        dialog.setTitle(resources.getString(R.string.screen_capturing_disabled_by_policy_dialog_title))
+        dialog.setMessage(
+            resources.getString(R.string.screen_capturing_disabled_by_policy_dialog_description)
+        )
+        dialog.setIcon(R.drawable.ic_cast)
+        dialog.setButton(BUTTON_POSITIVE, resources.getString(android.R.string.ok)) {
+            _, _ -> dialog.cancel()
+        }
+
+        return dialog
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 8b034b2..17f9caf 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -59,7 +59,7 @@
 import com.android.systemui.mediaprojection.SessionCreationSource;
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity;
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
-import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.AlertDialogWithDelegate;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -79,6 +79,7 @@
     private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver;
     private final StatusBarManager mStatusBarManager;
     private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
+    private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
 
     private String mPackageName;
     private int mUid;
@@ -93,14 +94,17 @@
     private boolean mUserSelectingTask = false;
 
     @Inject
-    public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
+    public MediaProjectionPermissionActivity(
+            FeatureFlags featureFlags,
             Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver,
             StatusBarManager statusBarManager,
-            MediaProjectionMetricsLogger mediaProjectionMetricsLogger) {
+            MediaProjectionMetricsLogger mediaProjectionMetricsLogger,
+            ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate) {
         mFeatureFlags = featureFlags;
         mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
         mStatusBarManager = statusBarManager;
         mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger;
+        mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate;
     }
 
     @Override
@@ -315,10 +319,7 @@
         final UserHandle hostUserHandle = getHostUserHandle();
         if (mScreenCaptureDevicePolicyResolver.get()
                 .isScreenCaptureCompletelyDisabled(hostUserHandle)) {
-            // Using application context for the dialog, instead of the activity context, so we get
-            // the correct screen width when in split screen.
-            Context dialogContext = getApplicationContext();
-            AlertDialog dialog = new ScreenCaptureDisabledDialog(dialogContext);
+            AlertDialog dialog = mScreenCaptureDisabledDialogDelegate.createDialog();
             setUpDialog(dialog);
             dialog.show();
             return true;
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt
index cc8cc51..d6629e0 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt
@@ -18,14 +18,18 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
 import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
 import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.withContext
 
 class TaskSwitcherNotificationViewModel
@@ -36,21 +40,30 @@
 ) {
 
     val uiState: Flow<TaskSwitcherNotificationUiState> =
-        interactor.taskSwitchChanges.map { taskSwitchChange ->
-            Log.d(TAG, "taskSwitchChange: $taskSwitchChange")
-            when (taskSwitchChange) {
-                is TaskSwitchState.TaskSwitched -> {
-                    TaskSwitcherNotificationUiState.Showing(
-                        projectedTask = taskSwitchChange.projectedTask,
-                        foregroundTask = taskSwitchChange.foregroundTask,
-                    )
-                }
-                is TaskSwitchState.NotProjectingTask,
-                is TaskSwitchState.TaskUnchanged -> {
-                    TaskSwitcherNotificationUiState.NotShowing
+        interactor.taskSwitchChanges
+            .map { taskSwitchChange ->
+                Log.d(TAG, "taskSwitchChange: $taskSwitchChange")
+                when (taskSwitchChange) {
+                    is TaskSwitchState.TaskSwitched -> {
+                        TaskSwitcherNotificationUiState.Showing(
+                            projectedTask = taskSwitchChange.projectedTask,
+                            foregroundTask = taskSwitchChange.foregroundTask,
+                        )
+                    }
+                    is TaskSwitchState.NotProjectingTask,
+                    is TaskSwitchState.TaskUnchanged -> {
+                        TaskSwitcherNotificationUiState.NotShowing
+                    }
                 }
             }
-        }
+            .transformLatest { uiState ->
+                emit(uiState)
+                if (uiState is TaskSwitcherNotificationUiState.Showing) {
+                    delay(NOTIFICATION_MAX_SHOW_DURATION)
+                    Log.d(TAG, "Auto hiding notification after $NOTIFICATION_MAX_SHOW_DURATION")
+                    emit(TaskSwitcherNotificationUiState.NotShowing)
+                }
+            }
 
     suspend fun onSwitchTaskClicked(task: RunningTaskInfo) {
         interactor.switchProjectedTask(task)
@@ -60,6 +73,7 @@
         withContext(backgroundDispatcher) { interactor.goBackToTask(task) }
 
     companion object {
+        @VisibleForTesting val NOTIFICATION_MAX_SHOW_DURATION = 5.seconds
         private const val TAG = "TaskSwitchNotifVM"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index 152f193..9f7d1b3 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
 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;
@@ -28,7 +29,6 @@
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -70,9 +70,11 @@
 
 import java.io.PrintWriter;
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
+
 @SysUISingleton
 public class NavigationBarControllerImpl implements
         ConfigurationController.ConfigurationListener,
@@ -82,7 +84,7 @@
     private static final String TAG = NavigationBarControllerImpl.class.getSimpleName();
 
     private final Context mContext;
-    private final Handler mHandler;
+    private final Executor mExecutor;
     private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
     private final SecureSettings mSecureSettings;
     private final DisplayTracker mDisplayTracker;
@@ -119,7 +121,7 @@
             NavigationModeController navigationModeController,
             SysUiState sysUiFlagsContainer,
             CommandQueue commandQueue,
-            @Main Handler mainHandler,
+            @Main Executor mainExecutor,
             ConfigurationController configurationController,
             NavBarHelper navBarHelper,
             TaskbarDelegate taskbarDelegate,
@@ -133,7 +135,7 @@
             SecureSettings secureSettings,
             DisplayTracker displayTracker) {
         mContext = context;
-        mHandler = mainHandler;
+        mExecutor = mainExecutor;
         mNavigationBarComponentFactory = navigationBarComponentFactory;
         mSecureSettings = secureSettings;
         mDisplayTracker = displayTracker;
@@ -193,7 +195,7 @@
         mNavMode = mode;
         updateAccessibilityButtonModeIfNeeded();
 
-        mHandler.post(() -> {
+        mExecutor.execute(() -> {
             // create/destroy nav bar based on nav mode only in unfolded state
             if (oldMode != mNavMode) {
                 updateNavbarForTaskbar();
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index c0e688f..c7aae3c 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -23,6 +23,7 @@
 import android.app.role.RoleManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService
 import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
@@ -37,20 +38,21 @@
 interface NoteTaskModule {
 
     @[Binds IntoMap ClassKey(NoteTaskControllerUpdateService::class)]
-    fun NoteTaskControllerUpdateService.bindNoteTaskControllerUpdateService(): Service
+    fun bindNoteTaskControllerUpdateService(service: NoteTaskControllerUpdateService): Service
 
-    @[Binds IntoMap ClassKey(NoteTaskBubblesController.NoteTaskBubblesService::class)]
-    fun NoteTaskBubblesController.NoteTaskBubblesService.bindNoteTaskBubblesService(): Service
+    @[Binds IntoMap ClassKey(NoteTaskBubblesService::class)]
+    fun bindNoteTaskBubblesService(service: NoteTaskBubblesService): Service
 
     @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
-    fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
+    fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity
 
     @[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)]
-    fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity():
-        Activity
+    fun bindLaunchNotesRoleSettingsTrampolineActivity(
+        activity: LaunchNotesRoleSettingsTrampolineActivity
+    ): Activity
 
     @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
-    fun CreateNoteTaskShortcutActivity.bindNoteTaskShortcutActivity(): Activity
+    fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity
 
     companion object {
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
index 2d63dbc..7d3f2a5 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
@@ -25,5 +25,7 @@
 interface NoteTaskQuickAffordanceModule {
 
     @[Binds IntoSet]
-    fun NoteTaskQuickAffordanceConfig.bindNoteTaskQuickAffordance(): KeyguardQuickAffordanceConfig
+    fun bindNoteTaskQuickAffordance(
+        impl: NoteTaskQuickAffordanceConfig
+    ): KeyguardQuickAffordanceConfig
 }
diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
index e1d1ec2..5432793d 100644
--- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
@@ -20,20 +20,24 @@
      * the [KeyguardTransitionInteractor].
      */
     internal val internalWakefulnessState: WakefulnessState = WakefulnessState.AWAKE,
-
     val lastWakeReason: WakeSleepReason = WakeSleepReason.OTHER,
     val lastSleepReason: WakeSleepReason = WakeSleepReason.OTHER,
 
-        /**
+    /**
      * Whether the power button double tap gesture was triggered since the last time went to sleep.
      * If this value is true while [isAsleep]=true, it means we'll be waking back up shortly. If it
      * is true while [isAwake]=true, it means we're awake because of the button gesture.
      *
-     * This value remains true until the next time [isAsleep]=true.
+     * This value remains true until the next time [isAsleep]=true, since it would otherwise be
+     * totally arbitrary at what point we decide the gesture was no longer "triggered". Since a
+     * sleep event is guaranteed to arrive prior to the next power button gesture (as the first tap
+     * of the double tap always begins a sleep transition), this will always be reset to false prior
+     * to a subsequent power gesture.
      */
     val powerButtonLaunchGestureTriggered: Boolean = false,
 ) {
-    fun isAwake() = internalWakefulnessState == WakefulnessState.AWAKE ||
+    fun isAwake() =
+        internalWakefulnessState == WakefulnessState.AWAKE ||
             internalWakefulnessState == WakefulnessState.STARTING_TO_WAKE
 
     fun isAsleep() = !isAwake()
@@ -48,11 +52,10 @@
     fun isAsleepFrom(wakeSleepReason: WakeSleepReason) =
         isAsleep() && lastSleepReason == wakeSleepReason
 
-    fun isAwakeOrAsleepFrom(reason: WakeSleepReason) =
-        isAsleepFrom(reason) || isAwakeFrom(reason)
+    fun isAwakeOrAsleepFrom(reason: WakeSleepReason) = isAsleepFrom(reason) || isAwakeFrom(reason)
 
     fun isAwakeFromTapOrGesture(): Boolean {
-        return isAwake() && (lastWakeReason == WakeSleepReason.TAP ||
-                lastWakeReason == WakeSleepReason.GESTURE)
+        return isAwake() &&
+            (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index 0dd0a60..52cf4ec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -481,7 +481,8 @@
                 mSecondaryMobileTitleText.setTextAppearance(
                         R.style.TextAppearance_InternetDialog_Active);
 
-                TextView mSecondaryMobileSummaryText = mDialogView.requireViewById(R.id.secondary_mobile_summary);
+                TextView mSecondaryMobileSummaryText =
+                        mDialogView.requireViewById(R.id.secondary_mobile_summary);
                 summary = getMobileNetworkSummary(autoSwitchNonDdsSubId);
                 if (!TextUtils.isEmpty(summary)) {
                     mSecondaryMobileSummaryText.setText(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileDataInteractor.kt
new file mode 100644
index 0000000..22bbbbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileDataInteractor.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.getBatteryLevel
+import com.android.systemui.util.kotlin.isBatteryPowerSaveEnabled
+import com.android.systemui.util.kotlin.isDevicePluggedIn
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+
+/** Observes BatterySaver mode state changes providing the [BatterySaverTileModel.Standard]. */
+open class BatterySaverTileDataInteractor
+@Inject
+constructor(
+    @Background private val bgCoroutineContext: CoroutineContext,
+    private val batteryController: BatteryController,
+) : QSTileDataInteractor<BatterySaverTileModel> {
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<BatterySaverTileModel> =
+        combine(
+            batteryController.isDevicePluggedIn().distinctUntilChanged().flowOn(bgCoroutineContext),
+            batteryController
+                .isBatteryPowerSaveEnabled()
+                .distinctUntilChanged()
+                .flowOn(bgCoroutineContext),
+            batteryController.getBatteryLevel().distinctUntilChanged().flowOn(bgCoroutineContext),
+        ) {
+            isPluggedIn: Boolean,
+            isPowerSaverEnabled: Boolean,
+            _, // we are only interested in battery level change, not the actual level
+            ->
+            BatterySaverTileModel.Standard(isPluggedIn, isPowerSaverEnabled)
+        }
+
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
new file mode 100644
index 0000000..1e4eb38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.policy.BatteryController
+import javax.inject.Inject
+
+/** Handles airplane mode tile clicks and long clicks. */
+class BatterySaverTileUserActionInteractor
+@Inject
+constructor(
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+    private val batteryController: BatteryController
+) : QSTileUserActionInteractor<BatterySaverTileModel> {
+
+    override suspend fun handleInput(input: QSTileInput<BatterySaverTileModel>) =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    if (!data.isPluggedIn) {
+                        batteryController.setPowerSaveMode(!data.isPowerSaving, action.view)
+                    }
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.view,
+                        Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
+                    )
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/model/BatterySaverTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/model/BatterySaverTileModel.kt
new file mode 100644
index 0000000..dbec50d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/model/BatterySaverTileModel.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.domain.model
+
+/** BatterySaver mode tile model. */
+sealed interface BatterySaverTileModel {
+
+    val isPluggedIn: Boolean
+    val isPowerSaving: Boolean
+
+    /** For when the device does not support extreme battery saver mode. */
+    data class Standard(
+        override val isPluggedIn: Boolean,
+        override val isPowerSaving: Boolean,
+    ) : BatterySaverTileModel
+
+    /**
+     * For when device supports extreme battery saver mode. Whether or not that mode is enabled is
+     * determined through [isExtremeSaving].
+     */
+    data class Extreme(
+        override val isPluggedIn: Boolean,
+        override val isPowerSaving: Boolean,
+        val isExtremeSaving: Boolean,
+    ) : BatterySaverTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
new file mode 100644
index 0000000..0c08fba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.ui
+
+import android.content.res.Resources
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [BatterySaverTileModel] to [QSTileState]. */
+open class BatterySaverTileMapper
+@Inject
+constructor(
+    @Main protected val resources: Resources,
+    private val theme: Resources.Theme,
+) : QSTileDataToStateMapper<BatterySaverTileModel> {
+
+    override fun map(config: QSTileConfig, data: BatterySaverTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            label = resources.getString(R.string.battery_detail_switch_title)
+            contentDescription = label
+
+            icon = {
+                Icon.Loaded(
+                    resources.getDrawable(
+                        if (data.isPowerSaving) R.drawable.qs_battery_saver_icon_on
+                        else R.drawable.qs_battery_saver_icon_off,
+                        theme
+                    ),
+                    null
+                )
+            }
+
+            sideViewIcon = QSTileState.SideViewIcon.None
+
+            if (data.isPluggedIn) {
+                activationState = QSTileState.ActivationState.UNAVAILABLE
+                supportedActions = setOf(QSTileState.UserAction.LONG_CLICK)
+                secondaryLabel = ""
+            } else if (data.isPowerSaving) {
+                activationState = QSTileState.ActivationState.ACTIVE
+                supportedActions =
+                    setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+
+                if (data is BatterySaverTileModel.Extreme) {
+                    secondaryLabel =
+                        resources.getString(
+                            if (data.isExtremeSaving) R.string.extreme_battery_saver_text
+                            else R.string.standard_battery_saver_text
+                        )
+                    stateDescription = secondaryLabel
+                } else {
+                    secondaryLabel = ""
+                }
+            } else {
+                activationState = QSTileState.ActivationState.INACTIVE
+                supportedActions =
+                    setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+                secondaryLabel = ""
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
new file mode 100644
index 0000000..caae4d2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain
+
+import android.content.Context
+import android.content.res.Resources
+import android.widget.Switch
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [InternetTileModel] to [QSTileState]. */
+class InternetTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Resources.Theme,
+    private val context: Context,
+) : QSTileDataToStateMapper<InternetTileModel> {
+
+    override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            label = resources.getString(R.string.quick_settings_internet_label)
+            expandedAccessibilityClass = Switch::class
+
+            if (data.secondaryLabel != null) {
+                secondaryLabel = data.secondaryLabel.loadText(context)
+            } else {
+                secondaryLabel = data.secondaryTitle
+            }
+
+            stateDescription = data.stateDescription.loadContentDescription(context)
+            contentDescription = data.contentDescription.loadContentDescription(context)
+
+            if (data.icon != null) {
+                this.icon = { data.icon }
+            } else if (data.iconId != null) {
+                val loadedIcon =
+                    Icon.Loaded(
+                        resources.getDrawable(data.iconId!!, theme),
+                        contentDescription = null
+                    )
+                this.icon = { loadedIcon }
+            }
+
+            sideViewIcon = QSTileState.SideViewIcon.Chevron
+
+            activationState =
+                if (data is InternetTileModel.Active) QSTileState.ActivationState.ACTIVE
+                else QSTileState.ActivationState.INACTIVE
+
+            supportedActions =
+                setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
new file mode 100644
index 0000000..fdc596b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.annotation.StringRes
+import android.content.Context
+import android.os.UserHandle
+import android.text.Html
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+/** Observes internet state changes providing the [InternetTileModel]. */
+class InternetTileDataInteractor
+@Inject
+constructor(
+    private val context: Context,
+    @Application private val scope: CoroutineScope,
+    airplaneModeRepository: AirplaneModeRepository,
+    private val connectivityRepository: ConnectivityRepository,
+    ethernetInteractor: EthernetInteractor,
+    mobileIconsInteractor: MobileIconsInteractor,
+    wifiInteractor: WifiInteractor,
+) : QSTileDataInteractor<InternetTileModel> {
+    private val internetLabel: String = context.getString(R.string.quick_settings_internet_label)
+
+    // Three symmetrical Flows that can be switched upon based on the value of
+    // [DefaultConnectionModel]
+    private val wifiIconFlow: Flow<InternetTileModel> =
+        wifiInteractor.wifiNetwork.flatMapLatest {
+            val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true)
+            if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) {
+                val secondary = removeDoubleQuotes(it.ssid)
+                flowOf(
+                    InternetTileModel.Active(
+                        secondaryTitle = secondary,
+                        icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
+                        stateDescription = wifiIcon.contentDescription,
+                        contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
+                    )
+                )
+            } else {
+                notConnectedFlow
+            }
+        }
+
+    private val mobileDataContentName: Flow<CharSequence?> =
+        mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
+            if (it == null) {
+                flowOf(null)
+            } else {
+                combine(it.isRoaming, it.networkTypeIconGroup) { isRoaming, networkTypeIconGroup ->
+                    val cd = loadString(networkTypeIconGroup.contentDescription)
+                    if (isRoaming) {
+                        val roaming = context.getString(R.string.data_connection_roaming)
+                        if (cd != null) {
+                            context.getString(R.string.mobile_data_text_format, roaming, cd)
+                        } else {
+                            roaming
+                        }
+                    } else {
+                        cd
+                    }
+                }
+            }
+        }
+
+    private val mobileIconFlow: Flow<InternetTileModel> =
+        mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
+            if (it == null) {
+                notConnectedFlow
+            } else {
+                combine(
+                    it.networkName,
+                    it.signalLevelIcon,
+                    mobileDataContentName,
+                ) { networkNameModel, signalIcon, dataContentDescription ->
+                    when (signalIcon) {
+                        is SignalIconModel.Cellular -> {
+                            val secondary =
+                                mobileDataContentConcat(
+                                    networkNameModel.name,
+                                    dataContentDescription
+                                )
+
+                            val stateLevel = signalIcon.level
+                            val drawable = SignalDrawable(context)
+                            drawable.setLevel(stateLevel)
+                            val loadedIcon = Icon.Loaded(drawable, null)
+
+                            InternetTileModel.Active(
+                                secondaryTitle = secondary,
+                                icon = loadedIcon,
+                                stateDescription = ContentDescription.Loaded(secondary.toString()),
+                                contentDescription = ContentDescription.Loaded(internetLabel),
+                            )
+                        }
+                        is SignalIconModel.Satellite -> {
+                            val secondary =
+                                signalIcon.icon.contentDescription.loadContentDescription(context)
+                            InternetTileModel.Active(
+                                secondaryTitle = secondary,
+                                iconId = signalIcon.icon.res,
+                                stateDescription = ContentDescription.Loaded(secondary),
+                                contentDescription = ContentDescription.Loaded(internetLabel),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+    private fun mobileDataContentConcat(
+        networkName: String?,
+        dataContentDescription: CharSequence?
+    ): CharSequence {
+        if (dataContentDescription == null) {
+            return networkName ?: ""
+        }
+        if (networkName == null) {
+            return Html.fromHtml(dataContentDescription.toString(), 0)
+        }
+
+        return Html.fromHtml(
+            context.getString(
+                R.string.mobile_carrier_text_format,
+                networkName,
+                dataContentDescription
+            ),
+            0
+        )
+    }
+
+    private fun loadString(@StringRes resId: Int): CharSequence? =
+        if (resId != 0) {
+            context.getString(resId)
+        } else {
+            null
+        }
+
+    private val ethernetIconFlow: Flow<InternetTileModel> =
+        ethernetInteractor.icon.flatMapLatest {
+            if (it == null) {
+                notConnectedFlow
+            } else {
+                val secondary = it.contentDescription
+                flowOf(
+                    InternetTileModel.Active(
+                        secondaryLabel = secondary?.toText(),
+                        iconId = it.res,
+                        stateDescription = null,
+                        contentDescription = secondary,
+                    )
+                )
+            }
+        }
+
+    private val notConnectedFlow: StateFlow<InternetTileModel> =
+        combine(
+                wifiInteractor.areNetworksAvailable,
+                airplaneModeRepository.isAirplaneMode,
+            ) { networksAvailable, isAirplaneMode ->
+                when {
+                    isAirplaneMode -> {
+                        val secondary = context.getString(R.string.status_bar_airplane)
+                        InternetTileModel.Inactive(
+                            secondaryTitle = secondary,
+                            iconId = R.drawable.ic_qs_no_internet_unavailable,
+                            stateDescription = null,
+                            contentDescription = ContentDescription.Loaded(secondary),
+                        )
+                    }
+                    networksAvailable -> {
+                        val secondary =
+                            context.getString(R.string.quick_settings_networks_available)
+                        InternetTileModel.Inactive(
+                            secondaryTitle = secondary,
+                            iconId = R.drawable.ic_qs_no_internet_available,
+                            stateDescription = null,
+                            contentDescription =
+                                ContentDescription.Loaded("$internetLabel,$secondary")
+                        )
+                    }
+                    else -> {
+                        NOT_CONNECTED_NETWORKS_UNAVAILABLE
+                    }
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), NOT_CONNECTED_NETWORKS_UNAVAILABLE)
+
+    /**
+     * Consumable flow describing the correct state for the InternetTile.
+     *
+     * Strict ordering of which repo is sending its data to the internet tile. Swaps between each of
+     * the interim providers (wifi, mobile, ethernet, or not-connected).
+     */
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<InternetTileModel> =
+        connectivityRepository.defaultConnections.flatMapLatest {
+            when {
+                it.ethernet.isDefault -> ethernetIconFlow
+                it.mobile.isDefault || it.carrierMerged.isDefault -> mobileIconFlow
+                it.wifi.isDefault -> wifiIconFlow
+                else -> notConnectedFlow
+            }
+        }
+
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+
+    private companion object {
+        val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
+            InternetTileModel.Inactive(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+            )
+
+        fun removeDoubleQuotes(string: String?): String? {
+            if (string == null) return null
+            return if (string.firstOrNull() == '"' && string.lastOrNull() == '"') {
+                string.substring(1, string.length - 1)
+            } else string
+        }
+
+        fun ContentDescription.toText(): Text =
+            when (this) {
+                is ContentDescription.Loaded -> Text.Loaded(this.description)
+                is ContentDescription.Resource -> Text.Resource(this.res)
+            }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
new file mode 100644
index 0000000..2620cd5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.connectivity.AccessPointController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+/** Handles internet tile clicks. */
+class InternetTileUserActionInteractor
+@Inject
+constructor(
+    @Main private val mainContext: CoroutineContext,
+    private val internetDialogManager: InternetDialogManager,
+    private val accessPointController: AccessPointController,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<InternetTileModel> {
+
+    override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    withContext(mainContext) {
+                        internetDialogManager.create(
+                            aboveStatusBar = true,
+                            accessPointController.canConfigMobileData(),
+                            accessPointController.canConfigWifi(),
+                            action.view,
+                        )
+                    }
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.view,
+                        Intent(Settings.ACTION_WIFI_SETTINGS)
+                    )
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
new file mode 100644
index 0000000..ece90461
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.model
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+
+/** Model describing the state that the QS Internet tile should be in. */
+sealed interface InternetTileModel {
+    val secondaryTitle: CharSequence?
+    val secondaryLabel: Text?
+    val iconId: Int?
+    val icon: Icon?
+    val stateDescription: ContentDescription?
+    val contentDescription: ContentDescription?
+
+    data class Active(
+        override val secondaryTitle: CharSequence? = null,
+        override val secondaryLabel: Text? = null,
+        override val iconId: Int? = null,
+        override val icon: Icon? = null,
+        override val stateDescription: ContentDescription? = null,
+        override val contentDescription: ContentDescription? = null,
+    ) : InternetTileModel
+
+    data class Inactive(
+        override val secondaryTitle: CharSequence? = null,
+        override val secondaryLabel: Text? = null,
+        override val iconId: Int? = null,
+        override val icon: Icon? = null,
+        override val stateDescription: ContentDescription? = null,
+        override val contentDescription: ContentDescription? = null,
+    ) : InternetTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 80f11f1..1c07d00 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -40,7 +40,7 @@
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.mediaprojection.SessionCreationSource
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
-import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate
 import com.android.systemui.qs.tiles.RecordIssueTile
 import com.android.systemui.res.R
 import com.android.systemui.screenrecord.RecordingService
@@ -65,6 +65,7 @@
     private val devicePolicyResolver: dagger.Lazy<ScreenCaptureDevicePolicyResolver>,
     private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
     private val userFileManager: UserFileManager,
+    private val screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate,
     @Assisted private val onStarted: Runnable,
 ) : SystemUIDialog.Delegate {
 
@@ -124,7 +125,7 @@
                         .isScreenCaptureCompletelyDisabled(UserHandle.of(userTracker.userId))
             ) {
                 mainExecutor.execute {
-                    ScreenCaptureDisabledDialog(context).show()
+                    screenCaptureDisabledDialogDelegate.createDialog().show()
                     screenRecordSwitch.isChecked = false
                 }
                 return@execute
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
index 22645c4..ea19020 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -71,7 +71,6 @@
 
     override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
         val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
-        val displayCutout = rootWindowInsets.displayCutout
         if (fitsSystemWindows) {
             val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom
 
@@ -79,23 +78,22 @@
             if (paddingChanged) {
                 setPadding(0, 0, 0, 0)
             }
-
-            val pairInsets: Pair<Int, Int> =
-                layoutInsetsController.getinsets(windowInsets, displayCutout)
-            leftInset = pairInsets.first
-            rightInset = pairInsets.second
-            applyMargins()
         } else {
             val changed =
                 paddingLeft != 0 || paddingRight != 0 || paddingTop != 0 || paddingBottom != 0
             if (changed) {
                 setPadding(0, 0, 0, 0)
             }
-
-            leftInset = 0
-            rightInset = 0
         }
+        leftInset = 0
+        rightInset = 0
 
+        val displayCutout = rootWindowInsets.displayCutout
+        val pairInsets: Pair<Int, Int> =
+            layoutInsetsController.getinsets(windowInsets, displayCutout)
+        leftInset = pairInsets.first
+        rightInset = pairInsets.second
+        applyMargins()
         return windowInsets
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index a4ba2a2..8fe84c9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -42,11 +42,9 @@
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
 import com.android.systemui.mediaprojection.SessionCreationSource;
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
-import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate;
 import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.CallbackController;
 
 import dagger.Lazy;
@@ -71,18 +69,19 @@
     private CountDownTimer mCountDownTimer = null;
     private final Executor mMainExecutor;
     private final BroadcastDispatcher mBroadcastDispatcher;
-    private final Context mContext;
     private final FeatureFlags mFlags;
-    private final UserContextProvider mUserContextProvider;
     private final UserTracker mUserTracker;
     private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
-    private final SystemUIDialog.Factory mDialogFactory;
+    private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
+    private final ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory;
+    private final ScreenRecordPermissionDialogDelegate.Factory
+            mScreenRecordPermissionDialogDelegateFactory;
 
     protected static final String INTENT_UPDATE_STATE =
             "com.android.systemui.screenrecord.UPDATE_STATE";
     protected static final String EXTRA_STATE = "extra_state";
 
-    private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners =
+    private final CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners =
             new CopyOnWriteArrayList<>();
 
     private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver;
@@ -115,24 +114,26 @@
      * Create a new RecordingController
      */
     @Inject
-    public RecordingController(@Main Executor mainExecutor,
+    public RecordingController(
+            @Main Executor mainExecutor,
             BroadcastDispatcher broadcastDispatcher,
-            Context context,
             FeatureFlags flags,
-            UserContextProvider userContextProvider,
             Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
             UserTracker userTracker,
             MediaProjectionMetricsLogger mediaProjectionMetricsLogger,
-            SystemUIDialog.Factory dialogFactory) {
+            ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate,
+            ScreenRecordDialogDelegate.Factory screenRecordDialogFactory,
+            ScreenRecordPermissionDialogDelegate.Factory
+                    screenRecordPermissionDialogDelegateFactory) {
         mMainExecutor = mainExecutor;
-        mContext = context;
         mFlags = flags;
         mDevicePolicyResolver = devicePolicyResolver;
         mBroadcastDispatcher = broadcastDispatcher;
-        mUserContextProvider = userContextProvider;
         mUserTracker = userTracker;
         mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger;
-        mDialogFactory = dialogFactory;
+        mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate;
+        mScreenRecordDialogFactory = screenRecordDialogFactory;
+        mScreenRecordPermissionDialogDelegateFactory = screenRecordPermissionDialogDelegateFactory;
 
         BroadcastOptions options = BroadcastOptions.makeBasic();
         options.setInteractive(true);
@@ -163,27 +164,18 @@
         if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)
                 && mDevicePolicyResolver.get()
                         .isScreenCaptureCompletelyDisabled(getHostUserHandle())) {
-            return new ScreenCaptureDisabledDialog(mContext);
+            return mScreenCaptureDisabledDialogDelegate.createDialog();
         }
 
         mMediaProjectionMetricsLogger.notifyProjectionInitiated(
                 getHostUid(), SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER);
 
-        return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
-                ? mDialogFactory.create(new ScreenRecordPermissionDialogDelegate(
-                getHostUserHandle(),
-                getHostUid(),
-                /* controller= */ this,
-                activityStarter,
-                mUserContextProvider,
-                onStartRecordingClicked,
-                mMediaProjectionMetricsLogger,
-                mDialogFactory))
-                : new ScreenRecordDialog(
-                        context,
-                        /* controller= */ this,
-                        mUserContextProvider,
-                        onStartRecordingClicked);
+        return (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
+                ? mScreenRecordPermissionDialogDelegateFactory
+                    .create(this, getHostUserHandle(), getHostUid(), onStartRecordingClicked)
+                : mScreenRecordDialogFactory
+                    .create(this, onStartRecordingClicked))
+                .createDialog();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialogDelegate.java
similarity index 77%
rename from packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
rename to packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialogDelegate.java
index b98093e..9f1447b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialogDelegate.java
@@ -49,52 +49,69 @@
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.util.Arrays;
 import java.util.List;
 
 /**
  * Dialog to select screen recording options
  */
-public class ScreenRecordDialog extends SystemUIDialog {
+public class ScreenRecordDialogDelegate implements SystemUIDialog.Delegate {
     private static final List<ScreenRecordingAudioSource> MODES = Arrays.asList(INTERNAL, MIC,
             MIC_AND_INTERNAL);
     private static final long DELAY_MS = 3000;
     private static final long INTERVAL_MS = 1000;
 
-    private final RecordingController mController;
+    private final SystemUIDialog.Factory mSystemUIDialogFactory;
     private final UserContextProvider mUserContextProvider;
-    @Nullable
+    private final RecordingController mController;
     private final Runnable mOnStartRecordingClicked;
     private Switch mTapsSwitch;
     private Switch mAudioSwitch;
     private Spinner mOptions;
 
-    public ScreenRecordDialog(Context context,
-                              RecordingController controller,
-                              UserContextProvider userContextProvider,
-                              @Nullable Runnable onStartRecordingClicked) {
-        super(context);
-        mController = controller;
+    @AssistedFactory
+    public interface Factory {
+        ScreenRecordDialogDelegate create(
+                RecordingController recordingController,
+                @Nullable Runnable onStartRecordingClicked
+        );
+    }
+
+    @AssistedInject
+    public ScreenRecordDialogDelegate(
+            SystemUIDialog.Factory systemUIDialogFactory,
+            UserContextProvider userContextProvider,
+            @Assisted RecordingController controller,
+            @Assisted @Nullable Runnable onStartRecordingClicked) {
+        mSystemUIDialogFactory = systemUIDialogFactory;
         mUserContextProvider = userContextProvider;
+        mController = controller;
         mOnStartRecordingClicked = onStartRecordingClicked;
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
+    public SystemUIDialog createDialog() {
+        return mSystemUIDialogFactory.create(this);
+    }
 
-        Window window = getWindow();
+    @Override
+    public void onCreate(SystemUIDialog dialog, Bundle savedInstanceState) {
+        Window window = dialog.getWindow();
 
         window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
 
         window.setGravity(Gravity.CENTER);
-        setTitle(R.string.screenrecord_title);
+        dialog.setTitle(R.string.screenrecord_title);
 
-        setContentView(R.layout.screen_record_dialog);
+        dialog.setContentView(R.layout.screen_record_dialog);
 
-        TextView cancelBtn = findViewById(R.id.button_cancel);
-        cancelBtn.setOnClickListener(v -> dismiss());
-        TextView startBtn = findViewById(R.id.button_start);
+        TextView cancelBtn = dialog.findViewById(R.id.button_cancel);
+        cancelBtn.setOnClickListener(v -> dialog.dismiss());
+        TextView startBtn = dialog.findViewById(R.id.button_start);
         startBtn.setOnClickListener(v -> {
             if (mOnStartRecordingClicked != null) {
                 // Note that it is important to run this callback before dismissing, so that the
@@ -104,13 +121,13 @@
 
             // Start full-screen recording
             requestScreenCapture(/* captureTarget= */ null);
-            dismiss();
+            dialog.dismiss();
         });
 
-        mAudioSwitch = findViewById(R.id.screenrecord_audio_switch);
-        mTapsSwitch = findViewById(R.id.screenrecord_taps_switch);
-        mOptions = findViewById(R.id.screen_recording_options);
-        ArrayAdapter a = new ScreenRecordingAdapter(getContext().getApplicationContext(),
+        mAudioSwitch = dialog.findViewById(R.id.screenrecord_audio_switch);
+        mTapsSwitch = dialog.findViewById(R.id.screenrecord_taps_switch);
+        mOptions = dialog.findViewById(R.id.screen_recording_options);
+        ArrayAdapter a = new ScreenRecordingAdapter(dialog.getContext().getApplicationContext(),
                 android.R.layout.simple_spinner_dropdown_item,
                 MODES);
         a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
index 3eb26f4..ba775cd3 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -46,17 +46,20 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserContextProvider
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 
 /** Dialog to select screen recording options */
-class ScreenRecordPermissionDialogDelegate(
-    private val hostUserHandle: UserHandle,
-    private val hostUid: Int,
-    private val controller: RecordingController,
+class ScreenRecordPermissionDialogDelegate @AssistedInject constructor(
+    @Assisted private val hostUserHandle: UserHandle,
+    @Assisted private val hostUid: Int,
+    @Assisted private val controller: RecordingController,
     private val activityStarter: ActivityStarter,
     private val userContextProvider: UserContextProvider,
-    private val onStartRecordingClicked: Runnable?,
+    @Assisted private val onStartRecordingClicked: Runnable?,
     mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
-    private val systemUIDialogFactory: SystemUIDialog.Factory
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
 ) :
     BaseMediaProjectionPermissionDialogDelegate<SystemUIDialog>(
         createOptionList(),
@@ -65,8 +68,19 @@
         mediaProjectionMetricsLogger,
         R.drawable.ic_screenrecord,
         R.color.screenrecord_icon_color
-    ),
-    SystemUIDialog.Delegate {
+    ), SystemUIDialog.Delegate {
+
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            recordingController: RecordingController,
+            hostUserHandle: UserHandle,
+            hostUid: Int,
+            onStartRecordingClicked: Runnable?
+        ): ScreenRecordPermissionDialogDelegate
+    }
+
     private lateinit var tapsSwitch: Switch
     private lateinit var tapsSwitchContainer: ViewGroup
     private lateinit var tapsView: View
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
new file mode 100644
index 0000000..2294fc0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.animation.Animator
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.Display
+import android.view.LayoutInflater
+import android.view.ScrollCaptureResponse
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.WindowInsets
+import android.window.OnBackInvokedDispatcher
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.res.R
+
+/**
+ * Legacy implementation of screenshot view methods. Just proxies the calls down into the original
+ * ScreenshotView.
+ */
+class LegacyScreenshotViewProxy(context: Context) : ScreenshotViewProxy {
+    override val view: ScreenshotView =
+        LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView
+    override val internalInsetsListener: ViewTreeObserver.OnComputeInternalInsetsListener
+    override val screenshotPreview: View
+
+    override var defaultDisplay: Int = Display.DEFAULT_DISPLAY
+        set(value) {
+            view.setDefaultDisplay(value)
+        }
+    override var defaultTimeoutMillis: Long = 6000
+        set(value) {
+            view.setDefaultTimeoutMillis(value)
+        }
+    override var onKeyListener: View.OnKeyListener? = null
+        set(value) {
+            view.setOnKeyListener(value)
+        }
+    override var flags: FeatureFlags? = null
+        set(value) {
+            view.setFlags(value)
+        }
+    override var packageName: String = ""
+        set(value) {
+            view.setPackageName(value)
+        }
+    override var logger: UiEventLogger? = null
+        set(value) {
+            view.setUiEventLogger(value)
+        }
+    override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
+        set(value) {
+            view.setCallbacks(value)
+        }
+    override var screenshot: ScreenshotData? = null
+        set(value) {
+            view.setScreenshot(value)
+        }
+
+    override val isAttachedToWindow
+        get() = view.isAttachedToWindow
+    override val isDismissing
+        get() = view.isDismissing
+    override val isPendingSharedTransition
+        get() = view.isPendingSharedTransition
+
+    init {
+        internalInsetsListener = view
+        screenshotPreview = view.screenshotPreview
+    }
+
+    override fun reset() = view.reset()
+    override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets)
+    override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets)
+
+    override fun badgeScreenshot(userBadgedIcon: Drawable) = view.badgeScreenshot(userBadgedIcon)
+
+    override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator =
+        view.createScreenshotDropInAnimation(screenRect, showFlash)
+
+    override fun addQuickShareChip(quickShareAction: Notification.Action) =
+        view.addQuickShareChip(quickShareAction)
+
+    override fun setChipIntents(imageData: ScreenshotController.SavedImageData) =
+        view.setChipIntents(imageData)
+
+    override fun animateDismissal() = view.animateDismissal()
+
+    override fun showScrollChip(packageName: String, onClick: Runnable) =
+        view.showScrollChip(packageName, onClick)
+
+    override fun hideScrollChip() = view.hideScrollChip()
+
+    override fun prepareScrollingTransition(
+        response: ScrollCaptureResponse,
+        screenBitmap: Bitmap,
+        newScreenshot: Bitmap,
+        screenshotTakenInPortrait: Boolean
+    ) =
+        view.prepareScrollingTransition(
+            response,
+            screenBitmap,
+            newScreenshot,
+            screenshotTakenInPortrait
+        )
+
+    override fun startLongScreenshotTransition(
+        transitionDestination: Rect,
+        onTransitionEnd: Runnable,
+        longScreenshot: ScrollCaptureController.LongScreenshot
+    ) = view.startLongScreenshotTransition(transitionDestination, onTransitionEnd, longScreenshot)
+
+    override fun restoreNonScrollingUi() = view.restoreNonScrollingUi()
+
+    override fun stopInputListening() = view.stopInputListening()
+
+    override fun requestFocus() {
+        view.requestFocus()
+    }
+
+    override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
+
+    override fun addOnAttachStateChangeListener(listener: View.OnAttachStateChangeListener) =
+        view.addOnAttachStateChangeListener(listener)
+
+    override fun findOnBackInvokedDispatcher(): OnBackInvokedDispatcher? =
+        view.findOnBackInvokedDispatcher()
+
+    override fun getViewTreeObserver(): ViewTreeObserver = view.viewTreeObserver
+
+    override fun post(runnable: Runnable) {
+        view.post(runnable)
+    }
+
+    class Factory : ScreenshotViewProxy.Factory {
+        override fun getProxy(context: Context): ScreenshotViewProxy {
+            return LegacyScreenshotViewProxy(context)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index ee3e7ba..13448d2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -65,7 +65,6 @@
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
 import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.ScrollCaptureResponse;
@@ -165,7 +164,7 @@
     /**
      * Structure returned by the SaveImageInBackgroundTask
      */
-    static class SavedImageData {
+    public static class SavedImageData {
         public Uri uri;
         public List<Notification.Action> smartActions;
         public Notification.Action quickShareAction;
@@ -237,6 +236,7 @@
 
     private final WindowContext mContext;
     private final FeatureFlags mFlags;
+    private final ScreenshotViewProxy mViewProxy;
     private final ScreenshotNotificationsController mNotificationsController;
     private final ScreenshotSmartActions mScreenshotSmartActions;
     private final UiEventLogger mUiEventLogger;
@@ -272,7 +272,6 @@
         respondToKeyDismissal();
     };
 
-    private ScreenshotView mScreenshotView;
     private final MessageContainerController mMessageContainerController;
     private Bitmap mScreenBitmap;
     private SaveImageInBackgroundTask mSaveInBgTask;
@@ -305,6 +304,7 @@
     ScreenshotController(
             Context context,
             FeatureFlags flags,
+            ScreenshotViewProxy.Factory viewProxyFactory,
             ScreenshotSmartActions screenshotSmartActions,
             ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory,
             ScrollCaptureClient scrollCaptureClient,
@@ -360,6 +360,8 @@
         mMessageContainerController = messageContainerController;
         mAssistContentRequester = assistContentRequester;
 
+        mViewProxy = viewProxyFactory.getProxy(mContext);
+
         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
 
         // Setup the window that we are going to use
@@ -461,7 +463,7 @@
 
         // The window is focusable by default
         setWindowFocusable(true);
-        mScreenshotView.requestFocus();
+        mViewProxy.requestFocus();
 
         enqueueScrollCaptureRequest(screenshot.getUserHandle());
 
@@ -485,10 +487,10 @@
             mMessageContainerController.onScreenshotTaken(screenshot);
         });
 
-        mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
+        mViewProxy.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
                 mContext.getDrawable(R.drawable.overlay_badge_background),
                 screenshot.getUserHandle()));
-        mScreenshotView.setScreenshot(screenshot);
+        mViewProxy.setScreenshot(screenshot);
 
         // ignore system bar insets for the purpose of window layout
         mWindow.getDecorView().setOnApplyWindowInsetsListener(
@@ -503,31 +505,31 @@
     void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) {
         withWindowAttached(() -> {
             if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
-                mScreenshotView.announceForAccessibility(mContext.getResources().getString(
+                mViewProxy.announceForAccessibility(mContext.getResources().getString(
                         R.string.screenshot_saving_work_profile_title));
             } else {
-                mScreenshotView.announceForAccessibility(
+                mViewProxy.announceForAccessibility(
                         mContext.getResources().getString(R.string.screenshot_saving_title));
             }
         });
 
-        mScreenshotView.reset();
+        mViewProxy.reset();
 
-        if (mScreenshotView.isAttachedToWindow()) {
+        if (mViewProxy.isAttachedToWindow()) {
             // if we didn't already dismiss for another reason
-            if (!mScreenshotView.isDismissing()) {
+            if (!mViewProxy.isDismissing()) {
                 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0,
                         oldPackageName);
             }
             if (DEBUG_WINDOW) {
                 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
-                        + "(dismissing=" + mScreenshotView.isDismissing() + ")");
+                        + "(dismissing=" + mViewProxy.isDismissing() + ")");
             }
         }
 
-        mScreenshotView.setPackageName(mPackageName);
+        mViewProxy.setPackageName(mPackageName);
 
-        mScreenshotView.updateOrientation(
+        mViewProxy.updateOrientation(
                 mWindowManager.getCurrentWindowMetrics().getWindowInsets());
     }
 
@@ -539,7 +541,7 @@
             Log.d(TAG, "dismissScreenshot");
         }
         // If we're already animating out, don't restart the animation
-        if (mScreenshotView.isDismissing()) {
+        if (mViewProxy.isDismissing()) {
             if (DEBUG_DISMISS) {
                 Log.v(TAG, "Already dismissing, ignoring duplicate command");
             }
@@ -547,11 +549,11 @@
         }
         mUiEventLogger.log(event, 0, mPackageName);
         mScreenshotHandler.cancelTimeout();
-        mScreenshotView.animateDismissal();
+        mViewProxy.animateDismissal();
     }
 
     boolean isPendingSharedTransition() {
-        return mScreenshotView.isPendingSharedTransition();
+        return mViewProxy.isPendingSharedTransition();
     }
 
     // Any cleanup needed when the service is being destroyed.
@@ -576,7 +578,7 @@
 
     private void releaseMediaPlayer() {
         if (mScreenshotSoundController == null) return;
-        mScreenshotSoundController.releaseScreenshotSound();
+        mScreenshotSoundController.releaseScreenshotSoundAsync();
     }
 
     private void respondToKeyDismissal() {
@@ -591,18 +593,15 @@
             Log.d(TAG, "reloadAssets()");
         }
 
-        // Inflate the screenshot layout
-        mScreenshotView = (ScreenshotView)
-                LayoutInflater.from(mContext).inflate(R.layout.screenshot, null);
-        mMessageContainerController.setView(mScreenshotView);
-        mScreenshotView.addOnAttachStateChangeListener(
+        mMessageContainerController.setView(mViewProxy.getView());
+        mViewProxy.addOnAttachStateChangeListener(
                 new View.OnAttachStateChangeListener() {
                     @Override
                     public void onViewAttachedToWindow(@NonNull View v) {
                         if (DEBUG_INPUT) {
                             Log.d(TAG, "Registering Predictive Back callback");
                         }
-                        mScreenshotView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                        mViewProxy.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(
                                 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback);
                     }
 
@@ -611,11 +610,12 @@
                         if (DEBUG_INPUT) {
                             Log.d(TAG, "Unregistering Predictive Back callback");
                         }
-                        mScreenshotView.findOnBackInvokedDispatcher()
+                        mViewProxy.findOnBackInvokedDispatcher()
                                 .unregisterOnBackInvokedCallback(mOnBackInvokedCallback);
                     }
                 });
-        mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() {
+        mViewProxy.setLogger(mUiEventLogger);
+        mViewProxy.setCallbacks(new ScreenshotView.ScreenshotViewCallback() {
             @Override
             public void onUserInteraction() {
                 if (DEBUG_INPUT) {
@@ -640,11 +640,12 @@
                 // TODO(159460485): Remove this when focus is handled properly in the system
                 setWindowFocusable(false);
             }
-        }, mFlags);
-        mScreenshotView.setDefaultDisplay(mDisplayId);
-        mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
+        });
+        mViewProxy.setFlags(mFlags);
+        mViewProxy.setDefaultDisplay(mDisplayId);
+        mViewProxy.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
 
-        mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
+        mViewProxy.setOnKeyListener((v, keyCode, event) -> {
             if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
                 if (DEBUG_INPUT) {
                     Log.d(TAG, "onKeyEvent: " + keyCode);
@@ -658,23 +659,24 @@
         if (DEBUG_WINDOW) {
             Log.d(TAG, "adding OnComputeInternalInsetsListener");
         }
-        mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView);
+        mViewProxy.getViewTreeObserver().addOnComputeInternalInsetsListener(
+                mViewProxy.getInternalInsetsListener());
         if (DEBUG_WINDOW) {
-            Log.d(TAG, "setContentView: " + mScreenshotView);
+            Log.d(TAG, "setContentView: " + mViewProxy.getView());
         }
-        setContentView(mScreenshotView);
+        setContentView(mViewProxy.getView());
     }
 
     private void prepareAnimation(Rect screenRect, boolean showFlash,
             Runnable onAnimationComplete) {
-        mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
+        mViewProxy.getViewTreeObserver().addOnPreDrawListener(
                 new ViewTreeObserver.OnPreDrawListener() {
                     @Override
                     public boolean onPreDraw() {
                         if (DEBUG_WINDOW) {
                             Log.d(TAG, "onPreDraw: startAnimation");
                         }
-                        mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
+                        mViewProxy.getViewTreeObserver().removeOnPreDrawListener(this);
                         startAnimation(screenRect, showFlash, onAnimationComplete);
                         return true;
                     }
@@ -694,13 +696,13 @@
                             if (mConfigChanges.applyNewConfig(mContext.getResources())) {
                                 // Hide the scroll chip until we know it's available in this
                                 // orientation
-                                mScreenshotView.hideScrollChip();
+                                mViewProxy.hideScrollChip();
                                 // Delay scroll capture eval a bit to allow the underlying activity
                                 // to set up in the new orientation.
                                 mScreenshotHandler.postDelayed(() -> {
                                     requestScrollCapture(owner);
                                 }, 150);
-                                mScreenshotView.updateInsets(
+                                mViewProxy.updateInsets(
                                         mWindowManager.getCurrentWindowMetrics().getWindowInsets());
                                 // Screenshot animation calculations won't be valid anymore,
                                 // so just end
@@ -759,16 +761,16 @@
                     + mLastScrollCaptureResponse.getWindowTitle() + "]");
 
             final ScrollCaptureResponse response = mLastScrollCaptureResponse;
-            mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
+            mViewProxy.showScrollChip(response.getPackageName(), /* onClick */ () -> {
                 DisplayMetrics displayMetrics = new DisplayMetrics();
                 getDisplay().getRealMetrics(displayMetrics);
                 Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId,
                         new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
 
-                mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
+                mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
                         mScreenshotTakenInPortrait);
                 // delay starting scroll capture to make sure the scrim is up before the app moves
-                mScreenshotView.post(() -> runBatchScrollCapture(response, owner));
+                mViewProxy.post(() -> runBatchScrollCapture(response, owner));
             });
         } catch (InterruptedException | ExecutionException e) {
             Log.e(TAG, "requestScrollCapture failed", e);
@@ -794,19 +796,19 @@
                 return;
             } catch (InterruptedException | ExecutionException e) {
                 Log.e(TAG, "Exception", e);
-                mScreenshotView.restoreNonScrollingUi();
+                mViewProxy.restoreNonScrollingUi();
                 return;
             }
 
             if (longScreenshot.getHeight() == 0) {
-                mScreenshotView.restoreNonScrollingUi();
+                mViewProxy.restoreNonScrollingUi();
                 return;
             }
 
             mLongScreenshotHolder.setLongScreenshot(longScreenshot);
             mLongScreenshotHolder.setTransitionDestinationCallback(
                     (transitionDestination, onTransitionEnd) -> {
-                        mScreenshotView.startLongScreenshotTransition(
+                        mViewProxy.startLongScreenshotTransition(
                                 transitionDestination, onTransitionEnd,
                                 longScreenshot);
                         // TODO: Do this via ActionIntentExecutor instead.
@@ -882,16 +884,14 @@
             }
             mWindowManager.removeViewImmediate(decorView);
         }
-        // Ensure that we remove the input monitor
-        if (mScreenshotView != null) {
-            mScreenshotView.stopInputListening();
-        }
+
+        mViewProxy.stopInputListening();
     }
 
     private void playCameraSoundIfNeeded() {
         if (mScreenshotSoundController == null) return;
         // the controller is not-null only on the default display controller
-        mScreenshotSoundController.playCameraSound();
+        mScreenshotSoundController.playScreenshotSoundAsync();
     }
 
     /**
@@ -932,7 +932,7 @@
         }
 
         mScreenshotAnimation =
-                mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash);
+                mViewProxy.createScreenshotDropInAnimation(screenRect, showFlash);
         if (onAnimationComplete != null) {
             mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
                 @Override
@@ -975,7 +975,7 @@
                 };
         Pair<ActivityOptions, ExitTransitionCoordinator> transition =
                 ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null,
-                        Pair.create(mScreenshotView.getScreenshotPreview(),
+                        Pair.create(mViewProxy.getScreenshotPreview(),
                                 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
 
         return transition;
@@ -999,7 +999,7 @@
             mCurrentRequestCallback.onFinish();
             mCurrentRequestCallback = null;
         }
-        mScreenshotView.reset();
+        mViewProxy.reset();
         removeWindow();
         mScreenshotHandler.cancelTimeout();
     }
@@ -1067,7 +1067,7 @@
     }
 
     private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
-        mScreenshotView.setChipIntents(imageData);
+        mViewProxy.setChipIntents(imageData);
     }
 
     /**
@@ -1084,11 +1084,11 @@
                         @Override
                         public void onAnimationEnd(Animator animation) {
                             super.onAnimationEnd(animation);
-                            mScreenshotView.addQuickShareChip(quickShareData.quickShareAction);
+                            mViewProxy.addQuickShareChip(quickShareData.quickShareAction);
                         }
                     });
                 } else {
-                    mScreenshotView.addQuickShareChip(quickShareData.quickShareAction);
+                    mViewProxy.addQuickShareChip(quickShareData.quickShareAction);
                 }
             });
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
index 2c0bdde..d3a7fc4a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
@@ -21,22 +21,34 @@
 import com.android.app.tracing.coroutines.async
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.google.errorprone.annotations.CanIgnoreReturnValue
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
 
 /** Controls sound reproduction after a screenshot is taken. */
 interface ScreenshotSoundController {
     /** Reproduces the camera sound. */
-    @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit>
+    suspend fun playScreenshotSound()
 
-    /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */
-    @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit>
+    /**
+     * Releases the sound. [playScreenshotSound] behaviour is undefined after this has been called.
+     */
+    suspend fun releaseScreenshotSound()
+
+    /** Reproduces the camera sound. Used for compatibility with Java code. */
+    fun playScreenshotSoundAsync()
+
+    /**
+     * Releases the sound. [playScreenshotSound] behaviour is undefined after this has been called.
+     * Used for compatibility with Java code.
+     */
+    fun releaseScreenshotSoundAsync()
 }
 
 class ScreenshotSoundControllerImpl
@@ -47,8 +59,8 @@
     @Background private val bgDispatcher: CoroutineDispatcher
 ) : ScreenshotSoundController {
 
-    val player: Deferred<MediaPlayer?> =
-        coroutineScope.async("loadCameraSound", bgDispatcher) {
+    private val player: Deferred<MediaPlayer?> =
+        coroutineScope.async("loadScreenshotSound", bgDispatcher) {
             try {
                 soundProvider.getScreenshotSound()
             } catch (e: IllegalStateException) {
@@ -57,8 +69,8 @@
             }
         }
 
-    override fun playCameraSound(): Deferred<Unit> {
-        return coroutineScope.async("playCameraSound", bgDispatcher) {
+    override suspend fun playScreenshotSound() {
+        withContext(bgDispatcher) {
             try {
                 player.await()?.start()
             } catch (e: IllegalStateException) {
@@ -68,8 +80,8 @@
         }
     }
 
-    override fun releaseScreenshotSound(): Deferred<Unit> {
-        return coroutineScope.async("releaseScreenshotSound", bgDispatcher) {
+    override suspend fun releaseScreenshotSound() {
+        withContext(bgDispatcher) {
             try {
                 withTimeout(1.seconds) { player.await()?.release() }
             } catch (e: TimeoutCancellationException) {
@@ -79,6 +91,14 @@
         }
     }
 
+    override fun playScreenshotSoundAsync() {
+        coroutineScope.launch { playScreenshotSound() }
+    }
+
+    override fun releaseScreenshotSoundAsync() {
+        coroutineScope.launch { releaseScreenshotSound() }
+    }
+
     private companion object {
         const val TAG = "ScreenshotSoundControllerImpl"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index be30a15..8a8766d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -102,7 +102,7 @@
 public class ScreenshotView extends FrameLayout implements
         ViewTreeObserver.OnComputeInternalInsetsListener {
 
-    interface ScreenshotViewCallback {
+    public interface ScreenshotViewCallback {
         void onUserInteraction();
 
         void onAction(Intent intent, UserHandle owner, boolean overrideTransition);
@@ -426,15 +426,15 @@
         return mScreenshotPreview;
     }
 
-    /**
-     * Set up the logger and callback on dismissal.
-     *
-     * Note: must be called before any other (non-constructor) method or null pointer exceptions
-     * may occur.
-     */
-    void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks, FeatureFlags flags) {
+    void setUiEventLogger(UiEventLogger uiEventLogger) {
         mUiEventLogger = uiEventLogger;
+    }
+
+    void setCallbacks(ScreenshotViewCallback callbacks) {
         mCallbacks = callbacks;
+    }
+
+    void setFlags(FeatureFlags flags) {
         mFlags = flags;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
new file mode 100644
index 0000000..0064521
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.animation.Animator
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.ScrollCaptureResponse
+import android.view.View
+import android.view.View.OnKeyListener
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.WindowInsets
+import android.window.OnBackInvokedDispatcher
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.flags.FeatureFlags
+
+/** Abstraction of the surface between ScreenshotController and ScreenshotView */
+interface ScreenshotViewProxy {
+    val view: ViewGroup
+    val internalInsetsListener: ViewTreeObserver.OnComputeInternalInsetsListener
+    val screenshotPreview: View
+
+    var defaultDisplay: Int
+    var defaultTimeoutMillis: Long
+    var onKeyListener: OnKeyListener?
+    var flags: FeatureFlags?
+    var packageName: String
+    var logger: UiEventLogger?
+    var callbacks: ScreenshotView.ScreenshotViewCallback?
+    var screenshot: ScreenshotData?
+
+    val isAttachedToWindow: Boolean
+    val isDismissing: Boolean
+    val isPendingSharedTransition: Boolean
+
+    fun reset()
+    fun updateInsets(insets: WindowInsets)
+    fun updateOrientation(insets: WindowInsets)
+    fun badgeScreenshot(userBadgedIcon: Drawable)
+    fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator
+    fun addQuickShareChip(quickShareAction: Notification.Action)
+    fun setChipIntents(imageData: ScreenshotController.SavedImageData)
+    fun animateDismissal()
+
+    fun showScrollChip(packageName: String, onClick: Runnable)
+    fun hideScrollChip()
+    fun prepareScrollingTransition(
+        response: ScrollCaptureResponse,
+        screenBitmap: Bitmap,
+        newScreenshot: Bitmap,
+        screenshotTakenInPortrait: Boolean
+    )
+    fun startLongScreenshotTransition(
+        transitionDestination: Rect,
+        onTransitionEnd: Runnable,
+        longScreenshot: ScrollCaptureController.LongScreenshot
+    )
+    fun restoreNonScrollingUi()
+
+    fun stopInputListening()
+    fun requestFocus()
+    fun announceForAccessibility(string: String)
+    fun addOnAttachStateChangeListener(listener: View.OnAttachStateChangeListener)
+    fun findOnBackInvokedDispatcher(): OnBackInvokedDispatcher?
+    fun getViewTreeObserver(): ViewTreeObserver
+    fun post(runnable: Runnable)
+
+    interface Factory {
+        fun getProxy(context: Context): ScreenshotViewProxy
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index bb34ede..8a2678c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -75,7 +75,7 @@
     private String mWindowOwner;
     private volatile boolean mCancelled;
 
-    static class LongScreenshot {
+    public static class LongScreenshot {
         private final ImageTileSet mImageTileSet;
         private final Session mSession;
         // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 3797b8b..a00c81d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -20,6 +20,7 @@
 
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
+import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
 import com.android.systemui.screenshot.RequestProcessor;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
@@ -29,12 +30,14 @@
 import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
 import com.android.systemui.screenshot.ScreenshotSoundProvider;
 import com.android.systemui.screenshot.ScreenshotSoundProviderImpl;
+import com.android.systemui.screenshot.ScreenshotViewProxy;
 import com.android.systemui.screenshot.TakeScreenshotService;
 import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
 import com.android.systemui.screenshot.appclips.AppClipsService;
 
 import dagger.Binds;
 import dagger.Module;
+import dagger.Provides;
 import dagger.multibindings.ClassKey;
 import dagger.multibindings.IntoMap;
 
@@ -81,4 +84,9 @@
     @Binds
     abstract ScreenshotSoundController bindScreenshotSoundController(
             ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
+
+    @Provides
+    static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory() {
+        return new LegacyScreenshotViewProxy.Factory();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
index d0585d3..20bd7c6 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
@@ -64,8 +64,6 @@
     private String mScrimName;
     private int mTintColor;
     private boolean mBlendWithMainColor = true;
-    private Runnable mChangeRunnable;
-    private Executor mChangeRunnableExecutor;
     private Executor mExecutor;
     private Looper mExecutorLooper;
     @Nullable
@@ -270,9 +268,6 @@
             mDrawable.invalidateSelf();
         }
 
-        if (mChangeRunnable != null) {
-            mChangeRunnableExecutor.execute(mChangeRunnable);
-        }
     }
 
     public int getTint() {
@@ -300,9 +295,6 @@
                 mViewAlpha = alpha;
 
                 mDrawable.setAlpha((int) (255 * alpha));
-                if (mChangeRunnable != null) {
-                    mChangeRunnableExecutor.execute(mChangeRunnable);
-                }
             }
         });
     }
@@ -311,14 +303,6 @@
         return mViewAlpha;
     }
 
-    /**
-     * Sets a callback that is invoked whenever the alpha, color, or tint change.
-     */
-    public void setChangeRunnable(Runnable changeRunnable, Executor changeRunnableExecutor) {
-        mChangeRunnable = changeRunnable;
-        mChangeRunnableExecutor = changeRunnableExecutor;
-    }
-
     @Override
     protected boolean canReceivePointerEvents() {
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cf7c3e0..2fd438b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -351,7 +351,7 @@
     private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate();
     private final NotificationGutsManager mGutsManager;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
-    private final QuickSettingsController mQsController;
+    private final QuickSettingsControllerImpl mQsController;
     private final NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
     private final TouchHandler mTouchHandler = new TouchHandler();
 
@@ -726,7 +726,7 @@
             TapAgainViewController tapAgainViewController,
             NavigationModeController navigationModeController,
             NavigationBarController navigationBarController,
-            QuickSettingsController quickSettingsController,
+            QuickSettingsControllerImpl quickSettingsController,
             FragmentService fragmentService,
             IStatusBarService statusBarService,
             ContentResolver contentResolver,
@@ -1288,9 +1288,10 @@
                             mView.getContext().getDisplay());
             mKeyguardStatusViewController = statusViewComponent.getKeyguardStatusViewController();
             mKeyguardStatusViewController.init();
+        }
 
-            mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
-            mKeyguardStatusViewController.getView().addOnLayoutChangeListener(
+        mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
+        mKeyguardStatusViewController.getView().addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     int oldHeight = oldBottom - oldTop;
                     if (v.getHeight() != oldHeight) {
@@ -1298,8 +1299,7 @@
                     }
                 });
 
-            updateClockAppearance();
-        }
+        updateClockAppearance();
     }
 
     @Override
@@ -1326,9 +1326,7 @@
 
     private void onSplitShadeEnabledChanged() {
         mShadeLog.logSplitShadeChanged(mSplitShadeEnabled);
-        if (!migrateClocksToBlueprint()) {
-            mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
-        }
+        mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
         // Reset any left over overscroll state. It is a rare corner case but can happen.
         mQsController.setOverScrollAmount(0);
         mScrimController.setNotificationsOverScrollAmount(0);
@@ -1443,13 +1441,11 @@
         mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
                 mStatusBarStateController.getInterpolatedDozeAmount());
 
-        if (!migrateClocksToBlueprint()) {
-            mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
-                    mBarState,
-                    false,
-                    false,
-                    mBarState);
-        }
+        mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
+                mBarState,
+                false,
+                false,
+                mBarState);
         if (mKeyguardQsUserSwitchController != null) {
             mKeyguardQsUserSwitchController.setKeyguardQsUserSwitchVisibility(
                     mBarState,
@@ -1669,11 +1665,13 @@
             mKeyguardStatusViewController.setLockscreenClockY(
                     mClockPositionAlgorithm.getExpandedPreferredClockY());
         }
-        if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) {
+        if (keyguardBottomAreaRefactor()) {
+            mKeyguardInteractor.setClockPosition(
+                mClockPositionResult.clockX, mClockPositionResult.clockY);
+        } else {
             mKeyguardBottomAreaInteractor.setClockPosition(
                 mClockPositionResult.clockX, mClockPositionResult.clockY);
         }
-
         boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
         boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
 
@@ -1751,11 +1749,13 @@
     }
 
     private void updateKeyguardStatusViewAlignment(boolean animate) {
-        if (migrateClocksToBlueprint()) {
-            return;
-        }
         boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
-        ConstraintLayout layout = mNotificationContainerParent;
+        ConstraintLayout layout;
+        if (migrateClocksToBlueprint()) {
+            layout = mKeyguardViewConfigurator.getKeyguardRootView();
+        } else {
+            layout = mNotificationContainerParent;
+        }
         mKeyguardStatusViewController.updateAlignment(
                 layout, mSplitShadeEnabled, shouldBeCentered, animate);
         mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
@@ -3316,9 +3316,6 @@
         /** Updates the views to the initial state for the fold to AOD animation. */
         @Override
         public void prepareFoldToAodAnimation() {
-            if (migrateClocksToBlueprint()) {
-                return;
-            }
             // Force show AOD UI even if we are not locked
             showAodUi();
 
@@ -3340,9 +3337,6 @@
         @Override
         public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
                 Runnable cancelAction) {
-            if (migrateClocksToBlueprint()) {
-                return;
-            }
             final ViewPropertyAnimator viewAnimator = mView.animate();
             viewAnimator.cancel();
             viewAnimator
@@ -3378,9 +3372,6 @@
         /** Cancels fold to AOD transition and resets view state. */
         @Override
         public void cancelFoldToAodAnimation() {
-            if (migrateClocksToBlueprint()) {
-                return;
-            }
             cancelAnimation();
             resetAlpha();
             resetTranslation();
@@ -4455,13 +4446,11 @@
                 }
             }
 
-            if (!migrateClocksToBlueprint()) {
-                mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
-                        statusBarState,
-                        keyguardFadingAway,
-                        goingToFullShade,
-                        mBarState);
-            }
+            mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
+                    statusBarState,
+                    keyguardFadingAway,
+                    goingToFullShade,
+                    mBarState);
 
             if (!keyguardBottomAreaRefactor()) {
                 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 8f9cef3..f7fed53 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -60,7 +60,6 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
-import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.scene.ui.view.WindowRootViewComponent;
 import com.android.systemui.settings.UserTracker;
@@ -507,7 +506,7 @@
 
     private void applyFitsSystemWindows(NotificationShadeWindowState state) {
         boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
-        if (!SceneContainerFlag.isEnabled() && mWindowRootView != null
+        if (mWindowRootView != null
                 && mWindowRootView.getFitsSystemWindows() != fitsSystemWindows) {
             mWindowRootView.setFitsSystemWindows(fitsSystemWindows);
             mWindowRootView.requestApplyInsets();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.kt
new file mode 100644
index 0000000..c96a339
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+interface QuickSettingsController {
+    /** Returns whether or not QuickSettings is expanded. */
+    val expanded: Boolean
+
+    /** Returns whether or not QuickSettings is being customized. */
+    val isCustomizing: Boolean
+
+    /** Returns Whether we should intercept a gesture to open Quick Settings. */
+    @Deprecated("specific to legacy touch handling")
+    fun shouldQuickSettingsIntercept(x: Float, y: Float, yDiff: Float): Boolean
+
+    /** Closes the Qs customizer. */
+    fun closeQsCustomizer()
+
+    /**
+     * This method closes QS but in split shade it should be used only in special cases: to make
+     * sure QS closes when shade is closed as well. Otherwise it will result in QS disappearing from
+     * split shade
+     */
+    @Deprecated("specific to legacy split shade") fun closeQs()
+
+    /** Calculate top padding for notifications */
+    @Deprecated("specific to legacy DebugDrawable")
+    fun calculateNotificationsTopPadding(
+        isShadeExpanding: Boolean,
+        keyguardNotificationStaticPadding: Int,
+        expandedFraction: Float,
+    ): Float
+
+    /** Calculate height of QS panel */
+    @Deprecated("specific to legacy DebugDrawable")
+    fun calculatePanelHeightExpanded(stackScrollerPadding: Int): Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
rename to packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index a5c0553..19d98a0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -118,7 +118,7 @@
  * TODO (b/264460656) make this dumpable
  */
 @SysUISingleton
-public class QuickSettingsController implements Dumpable {
+public class QuickSettingsControllerImpl implements QuickSettingsController, Dumpable {
     public static final String TAG = "QuickSettingsController";
 
     public static final int SHADE_BACK_ANIM_SCALE_MULTIPLIER = 100;
@@ -252,7 +252,7 @@
 
     /**
      * The window width currently in effect -- used together with
-     * {@link QuickSettingsController#mCachedGestureInsets} to decide whether a back gesture should
+     * {@link QuickSettingsControllerImpl#mCachedGestureInsets} to decide whether a back gesture should
      * receive a horizontal swipe inwards from the left/right vertical edge of the screen.
      * We cache this on ACTION_DOWN, and query it during both ACTION_DOWN and ACTION_MOVE events.
      */
@@ -304,7 +304,7 @@
     private final QS.ScrollListener mQsScrollListener = this::onScroll;
 
     @Inject
-    public QuickSettingsController(
+    public QuickSettingsControllerImpl(
             Lazy<NotificationPanelViewController> panelViewControllerLazy,
             NotificationPanelView panelView,
             QsFrameTranslateController qsFrameTranslateController,
@@ -399,23 +399,23 @@
         mQs = qs;
     }
 
-    public void setExpansionHeightListener(ExpansionHeightListener listener) {
+    void setExpansionHeightListener(ExpansionHeightListener listener) {
         mExpansionHeightListener = listener;
     }
 
-    public void setQsStateUpdateListener(QsStateUpdateListener listener) {
+    void setQsStateUpdateListener(QsStateUpdateListener listener) {
         mQsStateUpdateListener = listener;
     }
 
-    public void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) {
+    void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) {
         mApplyClippingImmediatelyListener = listener;
     }
 
-    public void setFlingQsWithoutClickListener(FlingQsWithoutClickListener listener) {
+    void setFlingQsWithoutClickListener(FlingQsWithoutClickListener listener) {
         mFlingQsWithoutClickListener = listener;
     }
 
-    public void setExpansionHeightSetToMaxListener(ExpansionHeightSetToMaxListener callback) {
+    void setExpansionHeightSetToMaxListener(ExpansionHeightSetToMaxListener callback) {
         mExpansionHeightSetToMaxListener = callback;
     }
 
@@ -507,15 +507,11 @@
                 && mPanelView.getRootWindowInsets().isVisible(ime());
     }
 
-    public boolean isExpansionEnabled() {
+    boolean isExpansionEnabled() {
         return mExpansionEnabledPolicy && mExpansionEnabledAmbient
             && !isRemoteInputActiveWithKeyboardUp();
     }
 
-    public float getTransitioningToFullShadeProgress() {
-        return mTransitioningToFullShadeProgress;
-    }
-
     /** */
     @VisibleForTesting
     boolean isExpandImmediate() {
@@ -536,7 +532,7 @@
      *  Computes (and caches) the gesture insets for the current window. Intended to be called
      *  on ACTION_DOWN, and safely queried repeatedly thereafter during ACTION_MOVE events.
      */
-    public void updateGestureInsetsCache() {
+    void updateGestureInsetsCache() {
         WindowManager wm = this.mPanelView.getContext().getSystemService(WindowManager.class);
         WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
         mCachedGestureInsets = windowMetrics.getWindowInsets().getInsets(
@@ -548,7 +544,7 @@
      *  Returns whether x coordinate lies in the vertical edges of the screen
      *  (the only place where a back gesture can be initiated).
      */
-    public boolean shouldBackBypassQuickSettings(float touchX) {
+    boolean shouldBackBypassQuickSettings(float touchX) {
         return (touchX < mCachedGestureInsets.left)
                 || (touchX > mCachedWindowWidth - mCachedGestureInsets.right);
     }
@@ -592,6 +588,7 @@
         return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag;
     }
 
+    @Override
     public boolean getExpanded() {
         return mShadeRepository.getLegacyIsQsExpanded().getValue();
     }
@@ -601,7 +598,7 @@
         return mShadeRepository.getLegacyQsTracking().getValue();
     }
 
-    public boolean getFullyExpanded() {
+    boolean getFullyExpanded() {
         return mFullyExpanded;
     }
 
@@ -623,27 +620,28 @@
         return mQs != null;
     }
 
+    @Override
     public boolean isCustomizing() {
         return isQsFragmentCreated() && mQs.isCustomizing();
     }
 
-    public float getExpansionHeight() {
+    float getExpansionHeight() {
         return mExpansionHeight;
     }
 
-    public boolean getExpandedWhenExpandingStarted() {
+    boolean getExpandedWhenExpandingStarted() {
         return mExpandedWhenExpandingStarted;
     }
 
-    public int getMinExpansionHeight() {
+    int getMinExpansionHeight() {
         return mMinExpansionHeight;
     }
 
-    public boolean isFullyExpandedAndTouchesDisallowed() {
+    boolean isFullyExpandedAndTouchesDisallowed() {
         return isQsFragmentCreated() && getFullyExpanded() && disallowTouches();
     }
 
-    public int getMaxExpansionHeight() {
+    int getMaxExpansionHeight() {
         return mMaxExpansionHeight;
     }
 
@@ -654,13 +652,14 @@
         return !mTouchAboveFalsingThreshold;
     }
 
-    public int getFalsingThreshold() {
+    int getFalsingThreshold() {
         return mFalsingThreshold;
     }
 
     /**
      * Returns Whether we should intercept a gesture to open Quick Settings.
      */
+    @Override
     public boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
         boolean keyguardShowing = mBarState == KEYGUARD;
         if (!isExpansionEnabled() || mCollapsedOnDown || (keyguardShowing
@@ -717,7 +716,7 @@
      * @param downY the y location where the touch started
      * Returns true if the panel could be collapsed because it stared on QQS
      */
-    public boolean canPanelCollapseOnQQS(float downX, float downY) {
+    boolean canPanelCollapseOnQQS(float downX, float downY) {
         if (mCollapsedOnDown || mBarState == KEYGUARD || getExpanded()) {
             return false;
         }
@@ -727,6 +726,7 @@
     }
 
     /** Closes the Qs customizer. */
+    @Override
     public void closeQsCustomizer() {
         if (mQs != null) {
             mQs.closeCustomizer();
@@ -734,7 +734,7 @@
     }
 
     /** Returns whether touches from the notification panel should be disallowed */
-    public boolean disallowTouches() {
+    boolean disallowTouches() {
         if (mQs != null) {
             return mQs.disallowPanelTouches();
         } else {
@@ -754,15 +754,11 @@
         }
     }
 
-    public void setDozing(boolean dozing) {
+    void setDozing(boolean dozing) {
         mDozing = dozing;
     }
 
-    /**
-     * This method closes QS but in split shade it should be used only in special cases: to make
-     * sure QS closes when shade is closed as well. Otherwise it will result in QS disappearing
-     * from split shade
-     */
+    @Override
     public void closeQs() {
         if (mSplitShadeEnabled) {
             mShadeLog.d("Closing QS while in split shade");
@@ -794,7 +790,7 @@
     }
 
     /** update Qs height state */
-    public void setExpansionHeight(float height) {
+    void setExpansionHeight(float height) {
         int maxHeight = getMaxExpansionHeight();
         height = Math.min(Math.max(
                 height, getMinExpansionHeight()), maxHeight);
@@ -817,7 +813,7 @@
     }
 
     /** */
-    public void setHeightOverrideToDesiredHeight() {
+    void setHeightOverrideToDesiredHeight() {
         if (isSizeChangeAnimationRunning() && isQsFragmentCreated()) {
             mQs.setHeightOverride(mQs.getDesiredHeight());
         }
@@ -919,7 +915,7 @@
     }
 
     /** Sets Qs ScrimEnabled and updates QS state. */
-    public void setScrimEnabled(boolean scrimEnabled) {
+    void setScrimEnabled(boolean scrimEnabled) {
         boolean changed = mScrimEnabled != scrimEnabled;
         mScrimEnabled = scrimEnabled;
         if (changed) {
@@ -995,7 +991,7 @@
     }
 
     /** update expanded state of QS */
-    public void updateExpansion() {
+    void updateExpansion() {
         if (mQs == null) return;
         final float squishiness;
         if ((isExpandImmediate() || getExpanded()) && !mSplitShadeEnabled) {
@@ -1053,13 +1049,13 @@
         // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade
         // transition. If that's not the case we should follow QS expansion fraction for when
         // user is pulling from the same top to go directly to expanded QS
-        return getTransitioningToFullShadeProgress() > 0
+        return mTransitioningToFullShadeProgress > 0
                 ? mLockscreenShadeTransitionController.getQSDragProgress()
                 : computeExpansionFraction();
     }
 
     /** */
-    public void updateExpansionEnabledAmbient() {
+    void updateExpansionEnabledAmbient() {
         final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsHeaderHeight;
         mExpansionEnabledAmbient = mSplitShadeEnabled
                 || (mAmbientState.getScrollY() <= scrollRangeToTop);
@@ -1081,7 +1077,7 @@
     }
 
     /** Calculate fraction of current QS expansion state */
-    public float computeExpansionFraction() {
+    float computeExpansionFraction() {
         if (mAnimatingHiddenFromCollapsed) {
             // When hiding QS from collapsed state, the expansion can sometimes temporarily
             // be larger than 0 because of the timing, leading to flickers.
@@ -1112,7 +1108,7 @@
     }
 
     /** Called when shade starts expanding. */
-    public void onExpandingStarted(boolean qsFullyExpanded) {
+    void onExpandingStarted(boolean qsFullyExpanded) {
         mNotificationStackScrollLayoutController.onExpansionStarted();
         mExpandedWhenExpandingStarted = qsFullyExpanded;
         mMediaHierarchyManager.setCollapsingShadeFromQS(mExpandedWhenExpandingStarted
@@ -1363,7 +1359,7 @@
         }
     }
 
-    /** Calculate top padding for notifications */
+    @Override
     public float calculateNotificationsTopPadding(boolean isShadeExpanding,
             int keyguardNotificationStaticPadding, float expandedFraction) {
         float topPadding;
@@ -1404,7 +1400,7 @@
         }
     }
 
-    /** Calculate height of QS panel */
+    @Override
     public int calculatePanelHeightExpanded(int stackScrollerPadding) {
         float
                 notificationHeight =
@@ -1576,7 +1572,6 @@
         }
     }
 
-    @VisibleForTesting
     boolean isTrackingBlocked() {
         return mConflictingExpansionGesture && getExpanded();
     }
@@ -1591,7 +1586,7 @@
     }
 
     /** handles touches in Qs panel area */
-    public boolean handleTouch(MotionEvent event, boolean isFullyCollapsed,
+    boolean handleTouch(MotionEvent event, boolean isFullyCollapsed,
             boolean isShadeOrQsHeightAnimationRunning) {
         if (isSplitShadeAndTouchXOutsideQs(event.getX())) {
             return false;
@@ -1747,7 +1742,7 @@
     }
 
     /** intercepts touches on Qs panel area. */
-    public boolean onIntercept(MotionEvent event) {
+    boolean onIntercept(MotionEvent event) {
         int pointerIndex = event.findPointerIndex(mTrackingPointer);
         if (pointerIndex < 0) {
             pointerIndex = 0;
@@ -1858,7 +1853,7 @@
      *
      * @param animateAway Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
      */
-    public void animateCloseQs(boolean animateAway) {
+    void animateCloseQs(boolean animateAway) {
         if (mExpansionAnimator != null) {
             if (!mAnimatorExpand) {
                 return;
@@ -1877,7 +1872,7 @@
     }
 
     /** @see #flingQs(float, int, Runnable, boolean) */
-    public void flingQs(float vel, int type) {
+    void flingQs(float vel, int type) {
         flingQs(vel, type, null /* onFinishRunnable */, false /* isClick */);
     }
 
@@ -2144,7 +2139,7 @@
     }
 
     /** */
-    public FragmentHostManager.FragmentListener getQsFragmentListener() {
+    FragmentHostManager.FragmentListener getQsFragmentListener() {
         return new QsFragmentListener();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
new file mode 100644
index 0000000..b8250cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import javax.inject.Inject
+
+@SysUISingleton
+class QuickSettingsControllerSceneImpl
+@Inject
+constructor(
+    private val shadeInteractor: ShadeInteractor,
+    private val qsSceneAdapter: QSSceneAdapter,
+    private val qsContainerController: QSContainerController,
+) : QuickSettingsController {
+
+    override val expanded: Boolean
+        get() = shadeInteractor.isQsExpanded.value
+
+    override val isCustomizing: Boolean
+        get() = qsSceneAdapter.isCustomizing.value
+
+    @Deprecated("specific to legacy touch handling")
+    override fun shouldQuickSettingsIntercept(x: Float, y: Float, yDiff: Float): Boolean {
+        throw UnsupportedOperationException()
+    }
+
+    override fun closeQsCustomizer() {
+        qsContainerController.setCustomizerShowing(false)
+    }
+
+    @Deprecated("specific to legacy split shade")
+    override fun closeQs() {
+        // Do nothing
+    }
+
+    @Deprecated("specific to legacy DebugDrawable")
+    override fun calculateNotificationsTopPadding(
+        isShadeExpanding: Boolean,
+        keyguardNotificationStaticPadding: Int,
+        expandedFraction: Float
+    ): Float {
+        throw UnsupportedOperationException()
+    }
+
+    @Deprecated("specific to legacy DebugDrawable")
+    override fun calculatePanelHeightExpanded(stackScrollerPadding: Int): Int {
+        throw UnsupportedOperationException()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 5632766..504dbfd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.shade
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.ui.adapter.QSSceneAdapterImpl
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.data.repository.PrivacyChipRepository
 import com.android.systemui.shade.data.repository.PrivacyChipRepositoryImpl
@@ -111,6 +113,26 @@
                 sceneContainerOff.get()
             }
         }
+
+        @Provides
+        @SysUISingleton
+        fun provideQuickSettingsController(
+            sceneContainerFlags: SceneContainerFlags,
+            sceneContainerOn: Provider<QuickSettingsControllerSceneImpl>,
+            sceneContainerOff: Provider<QuickSettingsControllerImpl>,
+        ): QuickSettingsController {
+            return if (sceneContainerFlags.isEnabled()) {
+                sceneContainerOn.get()
+            } else {
+                sceneContainerOff.get()
+            }
+        }
+
+        @Provides
+        @SysUISingleton
+        fun providesQSContainerController(impl: QSSceneAdapterImpl): QSContainerController {
+            return impl
+        }
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index ef4e530..a12b970 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -84,6 +84,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -457,14 +458,43 @@
         List<KeyboardShortcutMultiMappingGroup> keyboardShortcutMultiMappingGroups =
                 new ArrayList<>();
         for (KeyboardShortcutGroup group : keyboardShortcutGroups) {
-            CharSequence categoryTitle = group.getLabel();
-            List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>();
-            for (KeyboardShortcutInfo info : group.getItems()) {
-                shortcutMultiMappingInfos.add(new ShortcutMultiMappingInfo(info));
-            }
-            keyboardShortcutMultiMappingGroups.add(
+            KeyboardShortcutMultiMappingGroup mappedGroup =
                     new KeyboardShortcutMultiMappingGroup(
-                            categoryTitle, shortcutMultiMappingInfos));
+                            group.getLabel(),
+                            new ArrayList<>());
+            Map<String, List<ShortcutMultiMappingInfo>> shortcutMap = new LinkedHashMap<>();
+            for (KeyboardShortcutInfo info : group.getItems()) {
+                String label = info.getLabel().toString();
+                Icon icon = info.getIcon();
+                if (shortcutMap.containsKey(label)) {
+                    List<ShortcutMultiMappingInfo> shortcuts = shortcutMap.get(label);
+                    boolean foundSameIcon = false;
+                    for (ShortcutMultiMappingInfo shortcut : shortcuts) {
+                        Icon shortcutIcon = shortcut.getIcon();
+                        if ((shortcutIcon != null
+                                && icon != null
+                                && shortcutIcon.sameAs(icon))
+                                || (shortcutIcon == null && icon == null)) {
+                            foundSameIcon = true;
+                            shortcut.addShortcutKeyGroup(new ShortcutKeyGroup(info, null));
+                            break;
+                        }
+                    }
+                    if (!foundSameIcon) {
+                        shortcuts.add(new ShortcutMultiMappingInfo(info));
+                    }
+                } else {
+                    List<ShortcutMultiMappingInfo> shortcuts = new ArrayList<>();
+                    shortcuts.add(new ShortcutMultiMappingInfo(info));
+                    shortcutMap.put(label, shortcuts);
+                }
+            }
+            for (List<ShortcutMultiMappingInfo> shortcutInfos : shortcutMap.values()) {
+                for (ShortcutMultiMappingInfo shortcutInfo : shortcutInfos) {
+                    mappedGroup.addItem(shortcutInfo);
+                }
+            }
+            keyboardShortcutMultiMappingGroups.add(mappedGroup);
         }
         return keyboardShortcutMultiMappingGroups;
     }
@@ -1377,7 +1407,9 @@
         ShortcutMultiMappingInfo(KeyboardShortcutInfo info) {
             mLabel = info.getLabel();
             mIcon = info.getIcon();
-            mShortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(info, null));
+            mShortcutKeyGroups = new ArrayList<>(
+                Arrays.asList(new ShortcutKeyGroup(info, null))
+            );
         }
 
         CharSequence getLabel() {
@@ -1388,6 +1420,10 @@
             return mIcon;
         }
 
+        void addShortcutKeyGroup(ShortcutKeyGroup group) {
+            mShortcutKeyGroups.add(group);
+        }
+
         List<ShortcutKeyGroup> getShortcutKeyGroups() {
             return mShortcutKeyGroups;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 4ee8349..81f644f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -352,10 +352,6 @@
     /** Called by the touch helper when the drag down was aborted and should be reset. */
     internal fun onDragDownReset() {
         logger.logDragDownAborted()
-        nsslController.setDimmed(
-            /* dimmed= */ true,
-            /* animate= */ true,
-        )
         nsslController.resetScrollPosition()
         nsslController.resetCheckSnoozeLeavebehind()
         setDragDownAmountAnimated(0f)
@@ -366,12 +362,7 @@
      *
      * @param above whether they dragged above it
      */
-    internal fun onCrossedThreshold(above: Boolean) {
-        nsslController.setDimmed(
-            /* dimmed= */ !above,
-            /* animate= */ true,
-        )
-    }
+    internal fun onCrossedThreshold(above: Boolean) {}
 
     /** Called by the touch helper when the drag down was started */
     internal fun onDragDownStarted(startingChild: ExpandableView?) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 1a06eec..0091bc5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -420,7 +420,7 @@
         filter.addAction(Intent.ACTION_USER_UNLOCKED);
         filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
         filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
-        if (allowPrivateProfile()){
+        if (privateSpaceFlagsEnabled()) {
             filter.addAction(Intent.ACTION_PROFILE_AVAILABLE);
             filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE);
         }
@@ -813,13 +813,17 @@
     }
 
     private boolean profileAvailabilityActions(String action){
-        return allowPrivateProfile()?
+        return privateSpaceFlagsEnabled()?
                 Objects.equals(action,Intent.ACTION_PROFILE_AVAILABLE)||
                         Objects.equals(action,Intent.ACTION_PROFILE_UNAVAILABLE):
                 Objects.equals(action,Intent.ACTION_MANAGED_PROFILE_AVAILABLE)||
                         Objects.equals(action,Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
     }
 
+    private static boolean privateSpaceFlagsEnabled() {
+        return allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures();
+    }
+
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("NotificationLockscreenUserManager state:");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 642eacc..c4d9cbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -35,6 +35,10 @@
 import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
 import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
+import com.android.systemui.qs.tiles.impl.internet.domain.InternetTileMapper
+import com.android.systemui.qs.tiles.impl.internet.domain.interactor.InternetTileDataInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.interactor.InternetTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.qs.tiles.impl.saver.domain.DataSaverTileMapper
 import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileDataInteractor
 import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileUserActionInteractor
@@ -90,6 +94,7 @@
 
         const val AIRPLANE_MODE_TILE_SPEC = "airplane"
         const val DATA_SAVER_TILE_SPEC = "saver"
+        const val INTERNET_TILE_SPEC = "internet"
 
         /** Inject InternetTile or InternetTileNewImpl into tileMap in QSModule */
         @Provides
@@ -168,5 +173,36 @@
                 stateInteractor,
                 mapper,
             )
+
+        @Provides
+        @IntoMap
+        @StringKey(INTERNET_TILE_SPEC)
+        fun provideInternetTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(INTERNET_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.ic_qs_no_internet_available,
+                        labelRes = R.string.quick_settings_internet_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject InternetTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(INTERNET_TILE_SPEC)
+        fun provideInternetTileViewModel(
+            factory: QSTileViewModelFactory.Static<InternetTileModel>,
+            mapper: InternetTileMapper,
+            stateInteractor: InternetTileDataInteractor,
+            userActionInteractor: InternetTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(INTERNET_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt
new file mode 100644
index 0000000..1bebbfd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/**
+ * Whether to set the status bar keyguard view occluded or not, and whether to animate that change.
+ */
+data class OccludedState(
+    val occluded: Boolean,
+    val animate: Boolean = false,
+)
+
+/** Handles logic around calls to [StatusBarKeyguardViewManager] in legacy code. */
+@Deprecated("Will be removed once all of SBKVM's responsibilies are refactored.")
+@SysUISingleton
+class StatusBarKeyguardViewManagerInteractor
+@Inject
+constructor(
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    powerInteractor: PowerInteractor,
+) {
+    /** Occlusion state to apply whenever a keyguard transition is STARTED, if any. */
+    private val occlusionStateFromStartedStep: Flow<OccludedState> =
+        keyguardTransitionInteractor.startedKeyguardTransitionStep
+            .sample(powerInteractor.detailedWakefulness, ::Pair)
+            .map { (startedStep, wakefulness) ->
+                val transitioningFromPowerButtonGesture =
+                    KeyguardState.deviceIsAsleepInState(startedStep.from) &&
+                        startedStep.to == KeyguardState.OCCLUDED &&
+                        wakefulness.powerButtonLaunchGestureTriggered
+
+                if (
+                    startedStep.to == KeyguardState.OCCLUDED && !transitioningFromPowerButtonGesture
+                ) {
+                    // Set occluded upon STARTED, *unless* we're transitioning from the power
+                    // button, in which case we're going to play an animation over the lockscreen UI
+                    // and need to remain unoccluded until the transition finishes.
+                    return@map OccludedState(occluded = true, animate = false)
+                }
+
+                if (
+                    startedStep.from == KeyguardState.OCCLUDED &&
+                        startedStep.to == KeyguardState.LOCKSCREEN
+                ) {
+                    // Set unoccluded immediately ONLY if we're transitioning back to the lockscreen
+                    // since we need the views visible to animate them back down. This is a special
+                    // case due to the way unocclusion remote animations are run. We can remove this
+                    // once the unocclude animation uses the return animation framework.
+                    return@map OccludedState(occluded = false, animate = false)
+                }
+
+                // Otherwise, wait for the transition to FINISH to decide.
+                return@map null
+            }
+            .filterNotNull()
+
+    /** Occlusion state to apply whenever a keyguard transition is FINISHED. */
+    private val occlusionStateFromFinishedStep =
+        keyguardTransitionInteractor.finishedKeyguardTransitionStep
+            .sample(keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop, ::Pair)
+            .map { (finishedStep, showWhenLockedOnTop) ->
+                // If we're FINISHED in OCCLUDED, we want to render as occluded. We also need to
+                // remain occluded if a SHOW_WHEN_LOCKED activity is on the top of the task stack,
+                // and we're in any state other than GONE. This is necessary, for example, when we
+                // transition from OCCLUDED to a bouncer state. Otherwise, we should not be
+                // occluded.
+                val occluded =
+                    finishedStep.to == KeyguardState.OCCLUDED ||
+                        (showWhenLockedOnTop && finishedStep.to != KeyguardState.GONE)
+                OccludedState(occluded = occluded, animate = false)
+            }
+
+    /** Occlusion state to apply to SKBVM's setOccluded call. */
+    val keyguardViewOcclusionState =
+        merge(occlusionStateFromStartedStep, occlusionStateFromFinishedStep)
+            .distinctUntilChangedBy {
+                // Don't switch 'animate' values mid-transition.
+                it.occluded
+            }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index ea9df9a..05e8717 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -275,15 +275,6 @@
         return getHeight();
     }
 
-    /**
-     * Sets the notification as dimmed. The default implementation does nothing.
-     *
-     * @param dimmed Whether the notification should be dimmed.
-     * @param fade Whether an animation should be played to change the state.
-     */
-    public void setDimmed(boolean dimmed, boolean fade) {
-    }
-
     public boolean isRemoved() {
         return false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
index dab89c5..b90aa10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
@@ -19,7 +19,9 @@
 import android.widget.flags.Flags.notifLinearlayoutOptimized
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
 import javax.inject.Inject
+import javax.inject.Provider
 
 interface NotifRemoteViewsFactoryContainer {
     val factories: Set<NotifRemoteViewsFactory>
@@ -31,7 +33,8 @@
     featureFlags: FeatureFlags,
     precomputedTextViewFactory: PrecomputedTextViewFactory,
     bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory,
-    optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory
+    optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory,
+    notificationViewFlipperFactory: Provider<NotificationViewFlipperFactory>,
 ) : NotifRemoteViewsFactoryContainer {
     override val factories: Set<NotifRemoteViewsFactory> = buildSet {
         add(precomputedTextViewFactory)
@@ -41,5 +44,8 @@
         if (notifLinearlayoutOptimized()) {
             add(optimizedLinearLayoutFactory)
         }
+        if (NotificationViewFlipperPausing.isEnabled) {
+            add(notificationViewFlipperFactory.get())
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index d308fa5..f835cca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -42,6 +42,7 @@
 import android.view.ViewGroup;
 import android.widget.RemoteViews;
 
+import com.android.app.tracing.TraceUtils;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.widget.ImageMessageConsumer;
 import com.android.systemui.dagger.SysUISingleton;
@@ -369,49 +370,55 @@
             ExpandableNotificationRow row,
             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
             NotificationContentInflaterLogger logger) {
-        InflationProgress result = new InflationProgress();
-        final NotificationEntry entryForLogging = row.getEntry();
+        return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> {
+            InflationProgress result = new InflationProgress();
+            final NotificationEntry entryForLogging = row.getEntry();
 
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
-            result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
-        }
-
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
-            result.newExpandedView = createExpandedView(builder, isLowPriority);
-        }
-
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
-            result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
-        }
-
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
-            result.newPublicView = builder.makePublicContentView(isLowPriority);
-        }
-
-        if (AsyncGroupHeaderViewInflation.isEnabled()) {
-            if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging,
-                        "creating group summary remote view");
-                result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
+                result.newContentView = createContentView(builder, isLowPriority,
+                        usesIncreasedHeight);
             }
 
-            if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging,
-                        "creating low-priority group summary remote view");
-                result.mNewLowPriorityGroupHeaderView =
-                        builder.makeLowPriorityContentView(true /* useRegularSubtext */);
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
+                result.newExpandedView = createExpandedView(builder, isLowPriority);
             }
-        }
-        setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
-        result.packageContext = packageContext;
-        result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);
-        result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
-                true /* showingPublic */);
-        return result;
+
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
+                result.newHeadsUpView = builder.createHeadsUpContentView(
+                        usesIncreasedHeadsUpHeight);
+            }
+
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
+                result.newPublicView = builder.makePublicContentView(isLowPriority);
+            }
+
+            if (AsyncGroupHeaderViewInflation.isEnabled()) {
+                if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
+                    logger.logAsyncTaskProgress(entryForLogging,
+                            "creating group summary remote view");
+                    result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
+                }
+
+                if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
+                    logger.logAsyncTaskProgress(entryForLogging,
+                            "creating low-priority group summary remote view");
+                    result.mNewLowPriorityGroupHeaderView =
+                            builder.makeLowPriorityContentView(true /* useRegularSubtext */);
+                }
+            }
+            setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
+            result.packageContext = packageContext;
+            result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(
+                    false /* showingPublic */);
+            result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
+                    true /* showingPublic */);
+
+            return result;
+        });
     }
 
     private static void setNotifsViewsInflaterFactory(InflationProgress result,
@@ -445,6 +452,8 @@
             RemoteViews.InteractionHandler remoteViewClickHandler,
             @Nullable InflationCallback callback,
             NotificationContentInflaterLogger logger) {
+        Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
+
         NotificationContentView privateLayout = row.getPrivateLayout();
         NotificationContentView publicLayout = row.getPublicLayout();
         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
@@ -621,6 +630,7 @@
         cancellationSignal.setOnCancelListener(
                 () -> {
                     logger.logAsyncTaskProgress(entry, "apply cancelled");
+                    Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
                     runningInflations.values().forEach(CancellationSignal::cancel);
                 });
 
@@ -769,17 +779,17 @@
         if (!requiresHeightCheck(entry)) {
             return true;
         }
-        Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement");
-        int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
-        int referenceWidth = resources.getDimensionPixelSize(
-                R.dimen.notification_validation_reference_width);
-        int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY);
-        view.measure(widthSpec, heightSpec);
-        int minHeight = resources.getDimensionPixelSize(
-                R.dimen.notification_validation_minimum_allowed_height);
-        boolean result = view.getMeasuredHeight() >= minHeight;
-        Trace.endSection();
-        return result;
+        return TraceUtils.trace("NotificationContentInflater#satisfiesMinHeightRequirement", () -> {
+            int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+            int referenceWidth = resources.getDimensionPixelSize(
+                    R.dimen.notification_validation_reference_width);
+            int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth,
+                    View.MeasureSpec.EXACTLY);
+            view.measure(widthSpec, heightSpec);
+            int minHeight = resources.getDimensionPixelSize(
+                    R.dimen.notification_validation_minimum_allowed_height);
+            return view.getMeasuredHeight() >= minHeight;
+        });
     }
 
     /**
@@ -833,143 +843,144 @@
             @Nullable InflationCallback endListener, NotificationEntry entry,
             ExpandableNotificationRow row, NotificationContentInflaterLogger logger) {
         Assert.isMainThread();
+        if (!runningInflations.isEmpty()) {
+            return false;
+        }
         NotificationContentView privateLayout = row.getPrivateLayout();
         NotificationContentView publicLayout = row.getPublicLayout();
-        if (runningInflations.isEmpty()) {
-            logger.logAsyncTaskProgress(entry, "finishing");
-            boolean setRepliesAndActions = true;
-            if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
-                if (result.inflatedContentView != null) {
-                    // New view case
-                    privateLayout.setContractedChild(result.inflatedContentView);
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
-                            result.newContentView);
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
-                    // Reinflation case. Only update if it's still cached (i.e. view has not been
-                    // freed while inflating).
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
-                            result.newContentView);
-                }
-                setRepliesAndActions = true;
+        logger.logAsyncTaskProgress(entry, "finishing");
+        boolean setRepliesAndActions = true;
+        if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
+            if (result.inflatedContentView != null) {
+                // New view case
+                privateLayout.setContractedChild(result.inflatedContentView);
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
+                        result.newContentView);
+            } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
+                // Reinflation case. Only update if it's still cached (i.e. view has not been
+                // freed while inflating).
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
+                        result.newContentView);
             }
-
-            if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
-                if (result.inflatedExpandedView != null) {
-                    privateLayout.setExpandedChild(result.inflatedExpandedView);
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
-                            result.newExpandedView);
-                } else if (result.newExpandedView == null) {
-                    privateLayout.setExpandedChild(null);
-                    remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED);
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
-                            result.newExpandedView);
-                }
-                if (result.newExpandedView != null) {
-                    privateLayout.setExpandedInflatedSmartReplies(
-                            result.expandedInflatedSmartReplies);
-                } else {
-                    privateLayout.setExpandedInflatedSmartReplies(null);
-                }
-                row.setExpandable(result.newExpandedView != null);
-                setRepliesAndActions = true;
-            }
-
-            if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
-                if (result.inflatedHeadsUpView != null) {
-                    privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
-                            result.newHeadsUpView);
-                } else if (result.newHeadsUpView == null) {
-                    privateLayout.setHeadsUpChild(null);
-                    remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP);
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
-                            result.newHeadsUpView);
-                }
-                if (result.newHeadsUpView != null) {
-                    privateLayout.setHeadsUpInflatedSmartReplies(
-                            result.headsUpInflatedSmartReplies);
-                } else {
-                    privateLayout.setHeadsUpInflatedSmartReplies(null);
-                }
-                setRepliesAndActions = true;
-            }
-
-            if (AsyncHybridViewInflation.isEnabled()
-                    && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
-                HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder;
-                SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel;
-                if (viewHolder != null && viewModel != null) {
-                    if (viewModel.isConversation()) {
-                        SingleLineConversationViewBinder.bind(
-                                result.mInflatedSingleLineViewModel,
-                                result.mInflatedSingleLineViewHolder
-                        );
-                    } else {
-                        SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel,
-                                result.mInflatedSingleLineViewHolder);
-                    }
-                    privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder);
-                }
-            }
-
-            if (setRepliesAndActions) {
-                privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState);
-            }
-
-            if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
-                if (result.inflatedPublicView != null) {
-                    publicLayout.setContractedChild(result.inflatedPublicView);
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
-                            result.newPublicView);
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
-                    remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
-                            result.newPublicView);
-                }
-            }
-
-            if (AsyncGroupHeaderViewInflation.isEnabled()) {
-                if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
-                    if (result.mInflatedGroupHeaderView != null) {
-                        row.setIsLowPriority(false);
-                        row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView);
-                        remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
-                                result.mNewGroupHeaderView);
-                    } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) {
-                        // Re-inflation case. Only update if it's still cached (i.e. view has not
-                        // been freed while inflating).
-                        remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
-                                result.mNewGroupHeaderView);
-                    }
-                }
-
-                if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
-                    if (result.mInflatedLowPriorityGroupHeaderView != null) {
-                        // New view case, set row to low priority
-                        row.setIsLowPriority(true);
-                        row.setLowPriorityGroupHeader(
-                                /* headerView= */ result.mInflatedLowPriorityGroupHeaderView);
-                        remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
-                                result.mNewLowPriorityGroupHeaderView);
-                    } else if (remoteViewCache.hasCachedView(entry,
-                            FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) {
-                        // Re-inflation case. Only update if it's still cached (i.e. view has not
-                        // been freed while inflating).
-                        remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
-                                result.mNewGroupHeaderView);
-                    }
-                }
-            }
-
-            entry.headsUpStatusBarText = result.headsUpStatusBarText;
-            entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
-            if (endListener != null) {
-                endListener.onAsyncInflationFinished(entry);
-            }
-            return true;
+            setRepliesAndActions = true;
         }
-        return false;
+
+        if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
+            if (result.inflatedExpandedView != null) {
+                privateLayout.setExpandedChild(result.inflatedExpandedView);
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
+                        result.newExpandedView);
+            } else if (result.newExpandedView == null) {
+                privateLayout.setExpandedChild(null);
+                remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED);
+            } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
+                        result.newExpandedView);
+            }
+            if (result.newExpandedView != null) {
+                privateLayout.setExpandedInflatedSmartReplies(
+                        result.expandedInflatedSmartReplies);
+            } else {
+                privateLayout.setExpandedInflatedSmartReplies(null);
+            }
+            row.setExpandable(result.newExpandedView != null);
+            setRepliesAndActions = true;
+        }
+
+        if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
+            if (result.inflatedHeadsUpView != null) {
+                privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
+                        result.newHeadsUpView);
+            } else if (result.newHeadsUpView == null) {
+                privateLayout.setHeadsUpChild(null);
+                remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP);
+            } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
+                        result.newHeadsUpView);
+            }
+            if (result.newHeadsUpView != null) {
+                privateLayout.setHeadsUpInflatedSmartReplies(
+                        result.headsUpInflatedSmartReplies);
+            } else {
+                privateLayout.setHeadsUpInflatedSmartReplies(null);
+            }
+            setRepliesAndActions = true;
+        }
+
+        if (AsyncHybridViewInflation.isEnabled()
+                && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
+            HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder;
+            SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel;
+            if (viewHolder != null && viewModel != null) {
+                if (viewModel.isConversation()) {
+                    SingleLineConversationViewBinder.bind(
+                            result.mInflatedSingleLineViewModel,
+                            result.mInflatedSingleLineViewHolder
+                    );
+                } else {
+                    SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel,
+                            result.mInflatedSingleLineViewHolder);
+                }
+                privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder);
+            }
+        }
+
+        if (setRepliesAndActions) {
+            privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState);
+        }
+
+        if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
+            if (result.inflatedPublicView != null) {
+                publicLayout.setContractedChild(result.inflatedPublicView);
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
+                        result.newPublicView);
+            } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
+                remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
+                        result.newPublicView);
+            }
+        }
+
+        if (AsyncGroupHeaderViewInflation.isEnabled()) {
+            if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
+                if (result.mInflatedGroupHeaderView != null) {
+                    row.setIsLowPriority(false);
+                    row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView);
+                    remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
+                            result.mNewGroupHeaderView);
+                } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) {
+                    // Re-inflation case. Only update if it's still cached (i.e. view has not
+                    // been freed while inflating).
+                    remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
+                            result.mNewGroupHeaderView);
+                }
+            }
+
+            if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
+                if (result.mInflatedLowPriorityGroupHeaderView != null) {
+                    // New view case, set row to low priority
+                    row.setIsLowPriority(true);
+                    row.setLowPriorityGroupHeader(
+                            /* headerView= */ result.mInflatedLowPriorityGroupHeaderView);
+                    remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
+                            result.mNewLowPriorityGroupHeaderView);
+                } else if (remoteViewCache.hasCachedView(entry,
+                        FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) {
+                    // Re-inflation case. Only update if it's still cached (i.e. view has not
+                    // been freed while inflating).
+                    remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
+                            result.mNewGroupHeaderView);
+                }
+            }
+        }
+
+        entry.headsUpStatusBarText = result.headsUpStatusBarText;
+        entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
+        Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
+        if (endListener != null) {
+            endListener.onAsyncInflationFinished(entry);
+        }
+        return true;
     }
 
     private static RemoteViews createExpandedView(Notification.Builder builder,
@@ -1102,83 +1113,97 @@
         }
 
         @Override
+        protected void onPreExecute() {
+            Trace.beginAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
+        }
+
+        @Override
         protected InflationProgress doInBackground(Void... params) {
-            try {
-                final StatusBarNotification sbn = mEntry.getSbn();
-                // Ensure the ApplicationInfo is updated before a builder is recovered.
-                updateApplicationInfo(sbn);
-                final Notification.Builder recoveredBuilder
-                        = Notification.Builder.recoverBuilder(mContext,
-                        sbn.getNotification());
+            return TraceUtils.trace("NotificationContentInflater.AsyncInflationTask#doInBackground",
+                    () -> {
+                        try {
+                            return doInBackgroundInternal();
+                        } catch (Exception e) {
+                            mError = e;
+                            mLogger.logAsyncTaskException(mEntry, "inflating", e);
+                            return null;
+                        }
+                    });
+        }
 
-                Context packageContext = sbn.getPackageContext(mContext);
-                if (recoveredBuilder.usesTemplate()) {
-                    // For all of our templates, we want it to be RTL
-                    packageContext = new RtlEnabledContext(packageContext);
-                }
-                boolean isConversation = mEntry.getRanking().isConversation();
-                Notification.MessagingStyle messagingStyle = null;
-                if (isConversation) {
-                    messagingStyle = mConversationProcessor.processNotification(
-                            mEntry, recoveredBuilder, mLogger);
-                }
-                InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
-                        recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
-                        mUsesIncreasedHeadsUpHeight, packageContext, mRow,
-                        mNotifLayoutInflaterFactoryProvider, mLogger);
+        private InflationProgress doInBackgroundInternal() {
+            final StatusBarNotification sbn = mEntry.getSbn();
+            // Ensure the ApplicationInfo is updated before a builder is recovered.
+            updateApplicationInfo(sbn);
+            final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder(
+                    mContext, sbn.getNotification());
 
-                mLogger.logAsyncTaskProgress(mEntry,
-                        "getting existing smart reply state (on wrong thread!)");
-                InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
-                mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
-                InflationProgress result = inflateSmartReplyViews(
-                        /* result = */ inflationProgress,
-                        mReInflateFlags,
-                        mEntry,
-                        mContext,
-                        packageContext,
-                        previousSmartReplyState,
-                        mSmartRepliesInflater,
-                        mLogger);
-
-                if (AsyncHybridViewInflation.isEnabled()) {
-                    // Inflate the single-line content view's ViewModel and ViewHolder from the
-                    // background thread, the ViewHolder needs to be bind with ViewModel later from
-                    // the main thread.
-                    result.mInflatedSingleLineViewModel = SingleLineViewInflater
-                            .inflateSingleLineViewModel(
-                                    mEntry.getSbn().getNotification(),
-                                    messagingStyle,
-                                    recoveredBuilder,
-                                    mContext
-                            );
-                    result.mInflatedSingleLineViewHolder =
-                            SingleLineViewInflater.inflateSingleLineViewHolder(
-                                    isConversation,
-                                    mReInflateFlags,
-                                    mEntry,
-                                    mContext,
-                                    mLogger
-                            );
-                }
-
-                mLogger.logAsyncTaskProgress(mEntry,
-                        "getting row image resolver (on wrong thread!)");
-                final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
-                // wait for image resolver to finish preloading
-                mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
-                imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
-
-                return result;
-            } catch (Exception e) {
-                mError = e;
-                mLogger.logAsyncTaskException(mEntry, "inflating", e);
-                return null;
+            Context packageContext = sbn.getPackageContext(mContext);
+            if (recoveredBuilder.usesTemplate()) {
+                // For all of our templates, we want it to be RTL
+                packageContext = new RtlEnabledContext(packageContext);
             }
+            boolean isConversation = mEntry.getRanking().isConversation();
+            Notification.MessagingStyle messagingStyle = null;
+            if (isConversation) {
+                messagingStyle = mConversationProcessor.processNotification(
+                        mEntry, recoveredBuilder, mLogger);
+            }
+            InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
+                    recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
+                    mUsesIncreasedHeadsUpHeight, packageContext, mRow,
+                    mNotifLayoutInflaterFactoryProvider, mLogger);
+
+            mLogger.logAsyncTaskProgress(mEntry,
+                    "getting existing smart reply state (on wrong thread!)");
+            InflatedSmartReplyState previousSmartReplyState =
+                    mRow.getExistingSmartReplyState();
+            mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
+            InflationProgress result = inflateSmartReplyViews(
+                    /* result = */ inflationProgress,
+                    mReInflateFlags,
+                    mEntry,
+                    mContext,
+                    packageContext,
+                    previousSmartReplyState,
+                    mSmartRepliesInflater,
+                    mLogger);
+
+            if (AsyncHybridViewInflation.isEnabled()) {
+                // Inflate the single-line content view's ViewModel and ViewHolder from the
+                // background thread, the ViewHolder needs to be bind with ViewModel later from
+                // the main thread.
+                result.mInflatedSingleLineViewModel = SingleLineViewInflater
+                        .inflateSingleLineViewModel(
+                                mEntry.getSbn().getNotification(),
+                                messagingStyle,
+                                recoveredBuilder,
+                                mContext
+                        );
+                result.mInflatedSingleLineViewHolder =
+                        SingleLineViewInflater.inflateSingleLineViewHolder(
+                                isConversation,
+                                mReInflateFlags,
+                                mEntry,
+                                mContext,
+                                mLogger
+                        );
+            }
+
+            mLogger.logAsyncTaskProgress(mEntry,
+                    "getting row image resolver (on wrong thread!)");
+            final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
+            // wait for image resolver to finish preloading
+            mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
+            imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
+
+            return result;
         }
 
         @Override
         protected void onPostExecute(InflationProgress result) {
+            Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
+
             if (mError == null) {
                 // Logged in detail in apply.
                 mCancellationSignal = apply(
@@ -1197,6 +1222,11 @@
             }
         }
 
+        @Override
+        protected void onCancelled(InflationProgress result) {
+            Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
+        }
+
         private void handleError(Exception e) {
             mEntry.onInflationTaskFinished();
             StatusBarNotification sbn = mEntry.getSbn();
@@ -1294,4 +1324,8 @@
         public abstract void setResultView(View v);
         public abstract RemoteViews getRemoteView();
     }
+
+    private static final String ASYNC_TASK_TRACE_METHOD =
+            "NotificationContentInflater.AsyncInflationTask";
+    private static final String APPLY_TRACE_METHOD = "NotificationContentInflater#apply";
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt
new file mode 100644
index 0000000..0594c12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ViewFlipper
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.NotificationViewFlipperBinder
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.NotificationViewFlipperViewModel
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
+import javax.inject.Inject
+
+/**
+ * A factory which owns the construction of any ViewFlipper inside of Notifications, and binds it
+ * with a view model. This ensures that ViewFlippers are paused when the keyguard is showing.
+ */
+class NotificationViewFlipperFactory
+@Inject
+constructor(
+    private val viewModel: NotificationViewFlipperViewModel,
+) : NotifRemoteViewsFactory {
+    init {
+        /* check if */ NotificationViewFlipperPausing.isUnexpectedlyInLegacyMode()
+    }
+
+    override fun instantiate(
+        row: ExpandableNotificationRow,
+        @InflationFlag layoutType: Int,
+        parent: View?,
+        name: String,
+        context: Context,
+        attrs: AttributeSet
+    ): View? {
+        return when (name) {
+            ViewFlipper::class.java.name,
+            ViewFlipper::class.java.simpleName ->
+                ViewFlipper(context, attrs).also { viewFlipper ->
+                    NotificationViewFlipperBinder.bindWhileAttached(viewFlipper, viewModel)
+                }
+            else -> null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
index 9445d56..ea3036e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
@@ -31,6 +31,7 @@
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.time.SystemClock;
 
 import javax.inject.Inject;
 
@@ -46,9 +47,14 @@
     private NotificationEntry mEntry;
     private boolean mCancelled;
     private Throwable mInflateOrigin;
+    private final SystemClock mSystemClock;
+    private final RowInflaterTaskLogger mLogger;
+    private long mInflateStartTimeMs;
 
     @Inject
-    public RowInflaterTask() {
+    public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) {
+        mSystemClock = systemClock;
+        mLogger = logger;
     }
 
     /**
@@ -61,29 +67,49 @@
         }
         mListener = listener;
         AsyncLayoutInflater inflater = com.android.systemui.Flags.notificationRowUserContext()
-                ? new AsyncLayoutInflater(context, new RowAsyncLayoutInflater(entry))
+                ? new AsyncLayoutInflater(context, makeRowInflater(entry))
                 : new AsyncLayoutInflater(context);
         mEntry = entry;
         entry.setInflationTask(this);
+
+        mLogger.logInflateStart(entry);
+        mInflateStartTimeMs = mSystemClock.elapsedRealtime();
         inflater.inflate(R.layout.status_bar_notification_row, parent, this);
     }
 
+    private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) {
+        return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger);
+    }
+
     @VisibleForTesting
     static class RowAsyncLayoutInflater implements AsyncLayoutFactory {
         private final NotificationEntry mEntry;
+        private final SystemClock mSystemClock;
+        private final RowInflaterTaskLogger mLogger;
 
-        RowAsyncLayoutInflater(NotificationEntry entry) {
+        RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock,
+                RowInflaterTaskLogger logger) {
             mEntry = entry;
+            mSystemClock = systemClock;
+            mLogger = logger;
         }
 
         @Nullable
         @Override
         public View onCreateView(@Nullable View parent, @NonNull String name,
                 @NonNull Context context, @NonNull AttributeSet attrs) {
-            if (name.equals(ExpandableNotificationRow.class.getName())) {
-                return new ExpandableNotificationRow(context, attrs, mEntry);
+            if (!name.equals(ExpandableNotificationRow.class.getName())) {
+                return null;
             }
-            return null;
+
+            final long startMs = mSystemClock.elapsedRealtime();
+            final ExpandableNotificationRow row =
+                    new ExpandableNotificationRow(context, attrs, mEntry);
+            final long elapsedMs = mSystemClock.elapsedRealtime() - startMs;
+
+            mLogger.logCreatedRow(mEntry, elapsedMs);
+
+            return row;
         }
 
         @Nullable
@@ -101,6 +127,9 @@
 
     @Override
     public void onInflateFinished(View view, int resid, ViewGroup parent) {
+        final long elapsedMs = mSystemClock.elapsedRealtime() - mInflateStartTimeMs;
+        mLogger.logInflateFinish(mEntry, elapsedMs, mCancelled);
+
         if (!mCancelled) {
             try {
                 mEntry.onInflationTaskFinished();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTaskLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTaskLogger.kt
new file mode 100644
index 0000000..94da6be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTaskLogger.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.NotifInflationLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
+import javax.inject.Inject
+
+class RowInflaterTaskLogger @Inject constructor(@NotifInflationLog private val buffer: LogBuffer) {
+    fun logInflateStart(entry: NotificationEntry) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = entry.logKey },
+            { "started row inflation for $str1" }
+        )
+    }
+
+    fun logCreatedRow(entry: NotificationEntry, elapsedMs: Long) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = entry.logKey
+                long1 = elapsedMs
+            },
+            { "created row in $long1 ms for $str1" }
+        )
+    }
+
+    fun logInflateFinish(entry: NotificationEntry, elapsedMs: Long, cancelled: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = entry.logKey
+                long1 = elapsedMs
+                bool1 = cancelled
+            },
+            { "finished ${if (bool1) "cancelled " else ""}row inflation in $long1 ms for $str1" }
+        )
+    }
+}
+
+private const val TAG = "RowInflaterTask"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt
new file mode 100644
index 0000000..133d3e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import android.widget.ViewFlipper
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.NotificationViewFlipperViewModel
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationViewFlipper] to its [view model][NotificationViewFlipperViewModel]. */
+object NotificationViewFlipperBinder {
+    fun bindWhileAttached(
+        viewFlipper: ViewFlipper,
+        viewModel: NotificationViewFlipperViewModel,
+    ): DisposableHandle {
+        if (viewFlipper.isAutoStart) {
+            // If the ViewFlipper is not set to AutoStart, the pause binding is meaningless
+            return DisposableHandle {}
+        }
+        return viewFlipper.repeatWhenAttached {
+            lifecycleScope.launch { bind(viewFlipper, viewModel) }
+        }
+    }
+
+    suspend fun bind(
+        viewFlipper: ViewFlipper,
+        viewModel: NotificationViewFlipperViewModel,
+    ) = coroutineScope { launch { viewModel.isPaused.collect { viewFlipper.setPaused(it) } } }
+
+    private fun ViewFlipper.setPaused(paused: Boolean) {
+        if (paused) {
+            stopFlipping()
+        } else if (isAutoStart) {
+            startFlipping()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt
new file mode 100644
index 0000000..7694e58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
+import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import javax.inject.Inject
+
+/** A model which represents whether ViewFlippers inside notifications should be paused. */
+@SysUISingleton
+class NotificationViewFlipperViewModel
+@Inject
+constructor(
+    dumpManager: DumpManager,
+    stackInteractor: NotificationStackInteractor,
+) : FlowDumperImpl(dumpManager) {
+    init {
+        /* check if */ NotificationViewFlipperPausing.isUnexpectedlyInLegacyMode()
+    }
+
+    val isPaused = stackInteractor.isShowingOnLockscreen.dumpWhileCollecting("isPaused")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt
new file mode 100644
index 0000000..cea6a2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notification view flipper pausing flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationViewFlipperPausing {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationViewFlipperPausing()
+
+    /**
+     * 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)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index c90acee..ab2f664 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -61,7 +61,6 @@
      */
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private int mScrollY;
-    private boolean mDimmed;
     private float mOverScrollTopAmount;
     private float mOverScrollBottomAmount;
     private boolean mDozing;
@@ -344,14 +343,6 @@
         this.mScrollY = Math.max(scrollY, 0);
     }
 
-    /**
-     * @param dimmed Whether we are in a dimmed state (on the lockscreen), where the backgrounds are
-     *               translucent and everything is scaled back a bit.
-     */
-    public void setDimmed(boolean dimmed) {
-        mDimmed = dimmed;
-    }
-
     /** While dozing, we draw as little as possible, assuming a black background */
     public void setDozing(boolean dozing) {
         mDozing = dozing;
@@ -375,12 +366,6 @@
         mHideSensitive = hideSensitive;
     }
 
-    public boolean isDimmed() {
-        // While we are expanding from pulse, we want the notifications not to be dimmed, otherwise
-        // you'd see the difference to the pulsing notification
-        return mDimmed && !(isPulseExpanding() && mDozeAmount == 1.0f);
-    }
-
     public boolean isDozing() {
         return mDozing;
     }
@@ -768,7 +753,6 @@
         pw.println("mHideSensitive=" + mHideSensitive);
         pw.println("mShadeExpanded=" + mShadeExpanded);
         pw.println("mClearAllInProgress=" + mClearAllInProgress);
-        pw.println("mDimmed=" + mDimmed);
         pw.println("mStatusBarState=" + mStatusBarState);
         pw.println("mExpansionChanging=" + mExpansionChanging);
         pw.println("mPanelFullWidth=" + mIsSmallScreen);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
index 5343cbf..03a1082 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
@@ -35,7 +35,6 @@
     boolean animateZ;
     boolean animateHeight;
     boolean animateTopInset;
-    boolean animateDimmed;
     boolean animateHideSensitive;
     boolean hasDelays;
     boolean hasGoToFullShadeEvent;
@@ -83,11 +82,6 @@
         return this;
     }
 
-    public AnimationFilter animateDimmed() {
-        animateDimmed = true;
-        return this;
-    }
-
     public AnimationFilter animateHideSensitive() {
         animateHideSensitive = true;
         return this;
@@ -128,7 +122,6 @@
         animateZ |= filter.animateZ;
         animateHeight |= filter.animateHeight;
         animateTopInset |= filter.animateTopInset;
-        animateDimmed |= filter.animateDimmed;
         animateHideSensitive |= filter.animateHideSensitive;
         hasDelays |= filter.hasDelays;
         mAnimatedProperties.addAll(filter.mAnimatedProperties);
@@ -142,7 +135,6 @@
         animateZ = false;
         animateHeight = false;
         animateTopInset = false;
-        animateDimmed = false;
         animateHideSensitive = false;
         hasDelays = false;
         hasGoToFullShadeEvent = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
index d0c5c82..d1e5ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
@@ -88,7 +88,6 @@
             | ExpandableViewState.LOCATION_MAIN_AREA;
 
     public int height;
-    public boolean dimmed;
     public boolean hideSensitive;
     public boolean belowSpeedBump;
     public boolean inShelf;
@@ -128,7 +127,6 @@
         if (viewState instanceof ExpandableViewState) {
             ExpandableViewState svs = (ExpandableViewState) viewState;
             height = svs.height;
-            dimmed = svs.dimmed;
             hideSensitive = svs.hideSensitive;
             belowSpeedBump = svs.belowSpeedBump;
             clipTopAmount = svs.clipTopAmount;
@@ -155,9 +153,6 @@
                 expandableView.setActualHeight(newHeight, false /* notifyListeners */);
             }
 
-            // apply dimming
-            expandableView.setDimmed(this.dimmed, false /* animate */);
-
             // apply hiding sensitive
             expandableView.setHideSensitive(
                     this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */);
@@ -216,9 +211,6 @@
             abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET);
         }
 
-        // start dimmed animation
-        expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed);
-
         // apply below the speed bump
         if (!NotificationIconContainerRefactor.isEnabled()) {
             expandableView.setBelowSpeedBump(this.belowSpeedBump);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index fa97300..28f874d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -795,7 +795,6 @@
             } else {
                 childState.setZTranslation(0);
             }
-            childState.dimmed = parentState.dimmed;
             childState.hideSensitive = parentState.hideSensitive;
             childState.belowSpeedBump = parentState.belowSpeedBump;
             childState.clipTopAmount = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 27db84f..b47b18d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.stack;
 
 import static android.os.Trace.TRACE_TAG_APP;
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
@@ -29,10 +31,6 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.TimeAnimator;
-import android.animation.ValueAnimator;
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.FloatRange;
@@ -79,7 +77,6 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.keyguard.BouncerPanelExpansionCalculator;
@@ -163,18 +160,11 @@
      * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
      */
     private static final int INVALID_POINTER = -1;
-    /**
-     * The distance in pixels between sections when the sections are directly adjacent (no visible
-     * gap is drawn between them). In this case we don't want to round their corners.
-     */
-    private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1;
     private boolean mKeyguardBypassEnabled;
 
     private final ExpandHelper mExpandHelper;
     private NotificationSwipeHelper mSwipeHelper;
     private int mCurrentStackHeight = Integer.MAX_VALUE;
-    private final Paint mBackgroundPaint = new Paint();
-    private final boolean mShouldDrawNotificationBackground;
     private boolean mHighPriorityBeforeSpeedBump;
 
     private float mExpandedHeight;
@@ -263,7 +253,6 @@
     private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
     private boolean mNeedsAnimation;
     private boolean mTopPaddingNeedsAnimation;
-    private boolean mDimmedNeedsAnimation;
     private boolean mHideSensitiveNeedsAnimation;
     private boolean mActivateNeedsAnimation;
     private boolean mGoToFullShadeNeedsAnimation;
@@ -350,40 +339,15 @@
         }
     };
     private final NotificationSection[] mSections;
-    private boolean mAnimateNextBackgroundTop;
-    private boolean mAnimateNextBackgroundBottom;
-    private boolean mAnimateNextSectionBoundsChange;
-    private @ColorInt int mBgColor;
-    private float mDimAmount;
-    private ValueAnimator mDimAnimator;
     private final ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
-    private final Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            mDimAnimator = null;
-        }
-    };
-    private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener
-            = new ValueAnimator.AnimatorUpdateListener() {
-
-        @Override
-        public void onAnimationUpdate(ValueAnimator animation) {
-            setDimAmount((Float) animation.getAnimatedValue());
-        }
-    };
     protected ViewGroup mQsHeader;
     // Rect of QsHeader. Kept as a field just to avoid creating a new one each time.
     private final Rect mQsHeaderBound = new Rect();
     private boolean mContinuousShadowUpdate;
-    private boolean mContinuousBackgroundUpdate;
     private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> {
         updateViewShadows();
         return true;
     };
-    private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
-        updateBackground();
-        return true;
-    };
     private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
         float endY = view.getTranslationY() + view.getActualHeight();
         float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
@@ -481,7 +445,6 @@
     private boolean mHeadsUpAnimatingAway;
     private int mStatusBarState;
     private int mUpcomingStatusBarState;
-    private int mCachedBackgroundColor;
     private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
     private final Runnable mReflingAndAnimateScroll = this::animateScroll;
     private int mCornerRadius;
@@ -581,7 +544,6 @@
      */
     private boolean mDismissUsingRowTranslationX = true;
     private ExpandableNotificationRow mTopHeadsUpRow;
-    private long mNumHeadsUp;
     private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private boolean mShouldUseSplitNotificationShade;
@@ -595,7 +557,7 @@
         mSplitShadeStateController = splitShadeStateController;
         updateSplitNotificationShade();
     }
-    private FeatureFlags mFeatureFlags;
+    private final FeatureFlags mFeatureFlags;
 
     private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener =
             new ExpandableView.OnHeightChangedListener() {
@@ -667,8 +629,6 @@
         mSections = mSectionsManager.createSectionsForBuckets();
 
         mAmbientState = Dependency.get(AmbientState.class);
-        mBgColor = Utils.getColorAttr(mContext,
-                com.android.internal.R.attr.materialColorSurfaceContainerHigh).getDefaultColor();
         int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
         int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
         mSplitShadeMinContentHeight = res.getDimensionPixelSize(
@@ -680,16 +640,12 @@
 
         mStackScrollAlgorithm = createStackScrollAlgorithm(context);
         mStateAnimator = new StackStateAnimator(context, this);
-        mShouldDrawNotificationBackground =
-                res.getBoolean(R.bool.config_drawNotificationBackground);
         setOutlineProvider(mOutlineProvider);
 
         // We could set this whenever we 'requestChildUpdate' much like the viewTreeObserver, but
         // that adds a bunch of complexity, and drawing nothing isn't *that* expensive.
-        boolean willDraw = SceneContainerFlag.isEnabled()
-                || mShouldDrawNotificationBackground || mDebugLines;
+        boolean willDraw = SceneContainerFlag.isEnabled() || mDebugLines;
         setWillNotDraw(!willDraw);
-        mBackgroundPaint.setAntiAlias(true);
         if (mDebugLines) {
             mDebugPaint = new Paint();
             mDebugPaint.setColor(0xffff0000);
@@ -812,9 +768,6 @@
     }
 
     void updateBgColor() {
-        mBgColor = Utils.getColorAttr(mContext,
-                com.android.internal.R.attr.materialColorSurfaceContainerHigh).getDefaultColor();
-        updateBackgroundDimming();
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
             if (child instanceof ActivatableNotificationView activatableView) {
@@ -835,14 +788,6 @@
 
     protected void onDraw(Canvas canvas) {
         onJustBeforeDraw();
-        if (mShouldDrawNotificationBackground
-                && (mSections[0].getCurrentBounds().top
-                < mSections[mSections.length - 1].getCurrentBounds().bottom
-                || mAmbientState.isDozing())) {
-            drawBackground(canvas);
-        } else if (mInHeadsUpPinnedMode || mHeadsUpAnimatingAway) {
-            drawHeadsUpBackground(canvas);
-        }
 
         if (mDebugLines) {
             onDrawDebug(canvas);
@@ -930,150 +875,6 @@
         return textY;
     }
 
-    private void drawBackground(Canvas canvas) {
-        int lockScreenLeft = mSidePaddings;
-        int lockScreenRight = getWidth() - mSidePaddings;
-        int lockScreenTop = mSections[0].getCurrentBounds().top;
-        int lockScreenBottom = mSections[mSections.length - 1].getCurrentBounds().bottom;
-        int hiddenLeft = getWidth() / 2;
-        int hiddenTop = mTopPadding;
-
-        float yProgress = 1 - mInterpolatedHideAmount;
-        float xProgress = mHideXInterpolator.getInterpolation(
-                (1 - mLinearHideAmount) * mBackgroundXFactor);
-
-        int left = (int) MathUtils.lerp(hiddenLeft, lockScreenLeft, xProgress);
-        int right = (int) MathUtils.lerp(hiddenLeft, lockScreenRight, xProgress);
-        int top = (int) MathUtils.lerp(hiddenTop, lockScreenTop, yProgress);
-        int bottom = (int) MathUtils.lerp(hiddenTop, lockScreenBottom, yProgress);
-        mBackgroundAnimationRect.set(
-                left,
-                top,
-                right,
-                bottom);
-
-        int backgroundTopAnimationOffset = top - lockScreenTop;
-        // TODO(kprevas): this may not be necessary any more since we don't display the shelf in AOD
-        boolean anySectionHasVisibleChild = false;
-        for (NotificationSection section : mSections) {
-            if (section.needsBackground()) {
-                anySectionHasVisibleChild = true;
-                break;
-            }
-        }
-        boolean shouldDrawBackground;
-        if (mKeyguardBypassEnabled && onKeyguard()) {
-            shouldDrawBackground = isPulseExpanding();
-        } else {
-            shouldDrawBackground = !mAmbientState.isDozing() || anySectionHasVisibleChild;
-        }
-        if (shouldDrawBackground) {
-            drawBackgroundRects(canvas, left, right, top, backgroundTopAnimationOffset);
-        }
-
-        updateClipping();
-    }
-
-    /**
-     * Draws round rects for each background section.
-     * <p>
-     * We want to draw a round rect for each background section as defined by {@link #mSections}.
-     * However, if two sections are directly adjacent with no gap between them (e.g. on the
-     * lockscreen where the shelf can appear directly below the high priority section, or while
-     * scrolling the shade so that the top of the shelf is right at the bottom of the high priority
-     * section), we don't want to round the adjacent corners.
-     * <p>
-     * Since {@link Canvas} doesn't provide a way to draw a half-rounded rect, this means that we
-     * need to coalesce the backgrounds for adjacent sections and draw them as a single round rect.
-     * This method tracks the top of each rect we need to draw, then iterates through the visible
-     * sections.  If a section is not adjacent to the previous section, we draw the previous rect
-     * behind the sections we've accumulated up to that point, then start a new rect at the top of
-     * the current section.  When we're done iterating we will always have one rect left to draw.
-     */
-    private void drawBackgroundRects(Canvas canvas, int left, int right, int top,
-                                     int animationYOffset) {
-        int backgroundRectTop = top;
-        int lastSectionBottom =
-                mSections[0].getCurrentBounds().bottom + animationYOffset;
-        int currentLeft = left;
-        int currentRight = right;
-        boolean first = true;
-        for (NotificationSection section : mSections) {
-            if (!section.needsBackground()) {
-                continue;
-            }
-            int sectionTop = section.getCurrentBounds().top + animationYOffset;
-            int ownLeft = Math.min(Math.max(left, section.getCurrentBounds().left), right);
-            int ownRight = Math.max(Math.min(right, section.getCurrentBounds().right), ownLeft);
-            // If sections are directly adjacent to each other, we don't want to draw them
-            // as separate roundrects, as the rounded corners right next to each other look
-            // bad.
-            if (sectionTop - lastSectionBottom > DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX
-                    || ((currentLeft != ownLeft || currentRight != ownRight) && !first)) {
-                canvas.drawRoundRect(currentLeft,
-                        backgroundRectTop,
-                        currentRight,
-                        lastSectionBottom,
-                        mCornerRadius, mCornerRadius, mBackgroundPaint);
-                backgroundRectTop = sectionTop;
-            }
-            currentLeft = ownLeft;
-            currentRight = ownRight;
-            lastSectionBottom =
-                    section.getCurrentBounds().bottom + animationYOffset;
-            first = false;
-        }
-        canvas.drawRoundRect(currentLeft,
-                backgroundRectTop,
-                currentRight,
-                lastSectionBottom,
-                mCornerRadius, mCornerRadius, mBackgroundPaint);
-    }
-
-    private void drawHeadsUpBackground(Canvas canvas) {
-        int left = mSidePaddings;
-        int right = getWidth() - mSidePaddings;
-
-        float top = getHeight();
-        float bottom = 0;
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (child.getVisibility() != View.GONE
-                    && child instanceof ExpandableNotificationRow row) {
-                if ((row.isPinned() || row.isHeadsUpAnimatingAway()) && row.getTranslation() < 0
-                        && row.getProvider().shouldShowGutsOnSnapOpen()) {
-                    top = Math.min(top, row.getTranslationY());
-                    bottom = Math.max(bottom, row.getTranslationY() + row.getActualHeight());
-                }
-            }
-        }
-
-        if (top < bottom) {
-            canvas.drawRoundRect(
-                    left, top, right, bottom,
-                    mCornerRadius, mCornerRadius, mBackgroundPaint);
-        }
-    }
-
-    void updateBackgroundDimming() {
-        // No need to update the background color if it's not being drawn.
-        if (!mShouldDrawNotificationBackground) {
-            return;
-        }
-        // Interpolate between semi-transparent notification panel background color
-        // and white AOD separator.
-        float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */,
-                mLinearHideAmount);
-        int color = ColorUtils.blendARGB(mBgColor, Color.WHITE, colorInterpolation);
-
-        if (mCachedBackgroundColor != color) {
-            mCachedBackgroundColor = color;
-            mBackgroundPaint.setColor(color);
-            invalidate();
-        }
-    }
-
     private void reinitView() {
         initView(getContext(), mSwipeHelper, mNotificationStackSizeCalculator);
     }
@@ -1359,9 +1160,6 @@
 
     private void onPreDrawDuringAnimation() {
         mShelf.updateAppearance();
-        if (!mNeedsAnimation && !mChildrenUpdateRequested) {
-            updateBackground();
-        }
     }
 
     private void updateScrollStateForAddedChildren() {
@@ -2565,125 +2363,6 @@
         }
     }
 
-    private void updateBackground() {
-        // No need to update the background color if it's not being drawn.
-        if (!mShouldDrawNotificationBackground) {
-            return;
-        }
-
-        updateBackgroundBounds();
-        if (didSectionBoundsChange()) {
-            boolean animate = mAnimateNextSectionBoundsChange || mAnimateNextBackgroundTop
-                    || mAnimateNextBackgroundBottom || areSectionBoundsAnimating();
-            if (!isExpanded()) {
-                abortBackgroundAnimators();
-                animate = false;
-            }
-            if (animate) {
-                startBackgroundAnimation();
-            } else {
-                for (NotificationSection section : mSections) {
-                    section.resetCurrentBounds();
-                }
-                invalidate();
-            }
-        } else {
-            abortBackgroundAnimators();
-        }
-        mAnimateNextBackgroundTop = false;
-        mAnimateNextBackgroundBottom = false;
-        mAnimateNextSectionBoundsChange = false;
-    }
-
-    private void abortBackgroundAnimators() {
-        for (NotificationSection section : mSections) {
-            section.cancelAnimators();
-        }
-    }
-
-    private boolean didSectionBoundsChange() {
-        for (NotificationSection section : mSections) {
-            if (section.didBoundsChange()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean areSectionBoundsAnimating() {
-        for (NotificationSection section : mSections) {
-            if (section.areBoundsAnimating()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void startBackgroundAnimation() {
-        // TODO(kprevas): do we still need separate fields for top/bottom?
-        // or can each section manage its own animation state?
-        NotificationSection firstVisibleSection = getFirstVisibleSection();
-        NotificationSection lastVisibleSection = getLastVisibleSection();
-        for (NotificationSection section : mSections) {
-            section.startBackgroundAnimation(
-                    section == firstVisibleSection
-                            ? mAnimateNextBackgroundTop
-                            : mAnimateNextSectionBoundsChange,
-                    section == lastVisibleSection
-                            ? mAnimateNextBackgroundBottom
-                            : mAnimateNextSectionBoundsChange);
-        }
-    }
-
-    /**
-     * Update the background bounds to the new desired bounds
-     */
-    private void updateBackgroundBounds() {
-        int left = mSidePaddings;
-        int right = getWidth() - mSidePaddings;
-        for (NotificationSection section : mSections) {
-            section.getBounds().left = left;
-            section.getBounds().right = right;
-        }
-
-        if (!mIsExpanded) {
-            for (NotificationSection section : mSections) {
-                section.getBounds().top = 0;
-                section.getBounds().bottom = 0;
-            }
-            return;
-        }
-        int minTopPosition;
-        NotificationSection lastSection = getLastVisibleSection();
-        boolean onKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
-        if (!onKeyguard) {
-            minTopPosition = (int) (mTopPadding + mStackTranslation);
-        } else if (lastSection == null) {
-            minTopPosition = mTopPadding;
-        } else {
-            // The first sections could be empty while there could still be elements in later
-            // sections. The position of these first few sections is determined by the position of
-            // the first visible section.
-            NotificationSection firstVisibleSection = getFirstVisibleSection();
-            firstVisibleSection.updateBounds(0 /* minTopPosition*/, 0 /* minBottomPosition */,
-                    false /* shiftPulsingWithFirst */);
-            minTopPosition = firstVisibleSection.getBounds().top;
-        }
-        boolean shiftPulsingWithFirst = mNumHeadsUp <= 1
-                && (mAmbientState.isDozing() || (mKeyguardBypassEnabled && onKeyguard));
-        for (NotificationSection section : mSections) {
-            int minBottomPosition = minTopPosition;
-            if (section == lastSection) {
-                // We need to make sure the section goes all the way to the shelf
-                minBottomPosition = (int) (ViewState.getFinalTranslationY(mShelf)
-                        + mShelf.getIntrinsicHeight());
-            }
-            minTopPosition = section.updateBounds(minTopPosition, minBottomPosition,
-                    shiftPulsingWithFirst);
-            shiftPulsingWithFirst = false;
-        }
-    }
-
     private NotificationSection getFirstVisibleSection() {
         for (NotificationSection section : mSections) {
             if (section.getFirstVisibleChild() != null) {
@@ -3184,13 +2863,7 @@
                 mSections, getChildrenWithBackground());
 
         if (mAnimationsEnabled && mIsExpanded) {
-            mAnimateNextBackgroundTop = firstChild != previousFirstChild;
-            mAnimateNextBackgroundBottom = lastChild != previousLastChild || mAnimateBottomOnLayout;
-            mAnimateNextSectionBoundsChange = sectionViewsChanged;
         } else {
-            mAnimateNextBackgroundTop = false;
-            mAnimateNextBackgroundBottom = false;
-            mAnimateNextSectionBoundsChange = false;
         }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
         mAnimateBottomOnLayout = false;
@@ -3344,7 +3017,6 @@
             setAnimationRunning(true);
             mStateAnimator.startAnimationForEvents(mAnimationEvents, mGoToFullShadeDelay);
             mAnimationEvents.clear();
-            updateBackground();
             updateViewShadows();
         } else {
             applyCurrentState();
@@ -3359,7 +3031,6 @@
         generatePositionChangeEvents();
         generateTopPaddingEvent();
         generateActivateEvent();
-        generateDimmedEvent();
         generateHideSensitiveEvent();
         generateGoToFullShadeEvent();
         generateViewResizeEvent();
@@ -3577,14 +3248,6 @@
         mEverythingNeedsAnimation = false;
     }
 
-    private void generateDimmedEvent() {
-        if (mDimmedNeedsAnimation) {
-            mAnimationEvents.add(
-                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
-        }
-        mDimmedNeedsAnimation = false;
-    }
-
     private void generateHideSensitiveEvent() {
         if (mHideSensitiveNeedsAnimation) {
             mAnimationEvents.add(
@@ -3645,7 +3308,11 @@
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         if (SceneContainerFlag.isEnabled() && mIsBeingDragged) {
-            if (!mSendingTouchesToSceneFramework) {
+            int action = ev.getActionMasked();
+            boolean isUpOrCancel = action == ACTION_UP || action == ACTION_CANCEL;
+            if (mSendingTouchesToSceneFramework) {
+                mController.sendTouchToSceneFramework(ev);
+            } else if (!isUpOrCancel) {
                 // if this is the first touch being sent to the scene framework,
                 // convert it into a synthetic DOWN event.
                 mSendingTouchesToSceneFramework = true;
@@ -3653,14 +3320,9 @@
                 downEvent.setAction(MotionEvent.ACTION_DOWN);
                 mController.sendTouchToSceneFramework(downEvent);
                 downEvent.recycle();
-            } else {
-                mController.sendTouchToSceneFramework(ev);
             }
 
-            if (
-                    ev.getActionMasked() == MotionEvent.ACTION_UP
-                    || ev.getActionMasked() == MotionEvent.ACTION_CANCEL
-            ) {
+            if (isUpOrCancel) {
                 setIsBeingDragged(false);
             }
             return false;
@@ -3817,7 +3479,7 @@
                     }
                 }
                 break;
-            case MotionEvent.ACTION_UP:
+            case ACTION_UP:
                 if (mIsBeingDragged) {
                     final VelocityTracker velocityTracker = mVelocityTracker;
                     velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
@@ -3854,7 +3516,7 @@
                 }
 
                 break;
-            case MotionEvent.ACTION_CANCEL:
+            case ACTION_CANCEL:
                 if (mIsBeingDragged && getChildCount() > 0) {
                     if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
                             getScrollRange())) {
@@ -3963,7 +3625,7 @@
                     mTouchIsClick = false;
                 }
                 break;
-            case MotionEvent.ACTION_UP:
+            case ACTION_UP:
                 if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
                         isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
                     debugShadeLog("handleEmptySpaceClick: touch event propagated further");
@@ -4104,8 +3766,8 @@
                 break;
             }
 
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
+            case ACTION_CANCEL:
+            case ACTION_UP:
                 /* Release the drag */
                 setIsBeingDragged(false);
                 mActivePointerId = INVALID_POINTER;
@@ -4486,48 +4148,6 @@
         mAnimationFinishedRunnables.clear();
     }
 
-    /**
-     * See {@link AmbientState#setDimmed}.
-     */
-    void setDimmed(boolean dimmed, boolean animate) {
-        dimmed &= onKeyguard();
-        mAmbientState.setDimmed(dimmed);
-        if (animate && mAnimationsEnabled) {
-            mDimmedNeedsAnimation = true;
-            mNeedsAnimation = true;
-            animateDimmed(dimmed);
-        } else {
-            setDimAmount(dimmed ? 1.0f : 0.0f);
-        }
-        requestChildrenUpdate();
-    }
-
-    @VisibleForTesting
-    boolean isDimmed() {
-        return mAmbientState.isDimmed();
-    }
-
-    private void setDimAmount(float dimAmount) {
-        mDimAmount = dimAmount;
-        updateBackgroundDimming();
-    }
-
-    private void animateDimmed(boolean dimmed) {
-        if (mDimAnimator != null) {
-            mDimAnimator.cancel();
-        }
-        float target = dimmed ? 1.0f : 0.0f;
-        if (target == mDimAmount) {
-            return;
-        }
-        mDimAnimator = TimeAnimator.ofFloat(mDimAmount, target);
-        mDimAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED);
-        mDimAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        mDimAnimator.addListener(mDimEndListener);
-        mDimAnimator.addUpdateListener(mDimUpdateListener);
-        mDimAnimator.start();
-    }
-
     void updateSensitiveness(boolean animate, boolean hideSensitive) {
         if (hideSensitive != mAmbientState.isHideSensitive()) {
             int childCount = getChildCount();
@@ -4564,7 +4184,6 @@
 
         runAnimationFinishedRunnables();
         setAnimationRunning(false);
-        updateBackground();
         updateViewShadows();
     }
 
@@ -4714,7 +4333,6 @@
             invalidateOutline();
         }
         updateAlgorithmHeightAndPadding();
-        updateBackgroundDimming();
         requestChildrenUpdate();
         updateOwnTranslationZ();
     }
@@ -4747,21 +4365,6 @@
         }
     }
 
-    private int getNotGoneIndex(View child) {
-        int count = getChildCount();
-        int notGoneIndex = 0;
-        for (int i = 0; i < count; i++) {
-            View v = getChildAt(i);
-            if (child == v) {
-                return notGoneIndex;
-            }
-            if (v.getVisibility() != View.GONE) {
-                notGoneIndex++;
-            }
-        }
-        return -1;
-    }
-
     /**
      * Returns whether or not a History button is shown in the footer. If there is no footer, then
      * this will return false.
@@ -5266,13 +4869,10 @@
     void onStatePostChange(boolean fromShadeLocked) {
         boolean onKeyguard = onKeyguard();
 
-        mAmbientState.setDimmed(onKeyguard);
-
         if (mHeadsUpAppearanceController != null) {
             mHeadsUpAppearanceController.onStateChanged();
         }
 
-        setDimmed(onKeyguard, fromShadeLocked);
         setExpandingEnabled(!onKeyguard);
         if (!FooterViewRefactor.isEnabled()) {
             updateFooter();
@@ -5676,7 +5276,6 @@
      */
     public void setDozeAmount(float dozeAmount) {
         mAmbientState.setDozeAmount(dozeAmount);
-        updateContinuousBackgroundDrawing();
         updateStackPosition();
         requestChildrenUpdate();
     }
@@ -5711,7 +5310,6 @@
                 view.setTranslationY(wakeUplocation);
             }
         }
-        mDimmedNeedsAnimation = true;
     }
 
     void setAnimateBottomOnLayout(boolean animateBottomOnLayout) {
@@ -5763,7 +5361,6 @@
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
         updateContinuousShadowDrawing();
-        updateContinuousBackgroundDrawing();
         requestChildrenUpdate();
     }
 
@@ -5786,7 +5383,6 @@
      * @param numHeadsUp the number of active alerting notifications.
      */
     public void setNumHeadsUp(long numHeadsUp) {
-        mNumHeadsUp = numHeadsUp;
         mAmbientState.setHasHeadsUpEntries(numHeadsUp > 0);
     }
 
@@ -6160,19 +5756,6 @@
         mSpeedBumpIndexDirty = true;
     }
 
-    void updateContinuousBackgroundDrawing() {
-        boolean continuousBackground = !mAmbientState.isFullyAwake()
-                && mSwipeHelper.isSwiping();
-        if (continuousBackground != mContinuousBackgroundUpdate) {
-            mContinuousBackgroundUpdate = continuousBackground;
-            if (continuousBackground) {
-                getViewTreeObserver().addOnPreDrawListener(mBackgroundUpdater);
-            } else {
-                getViewTreeObserver().removeOnPreDrawListener(mBackgroundUpdater);
-            }
-        }
-    }
-
     private void resetAllSwipeState() {
         Trace.beginSection("NSSL.resetAllSwipeState()");
         mSwipeHelper.resetTouchState();
@@ -6259,7 +5842,6 @@
                         .animateHeight()
                         .animateTopInset()
                         .animateY()
-                        .animateDimmed()
                         .animateZ(),
 
                 // ANIMATION_TYPE_ACTIVATED_CHILD
@@ -6267,8 +5849,7 @@
                         .animateZ(),
 
                 // ANIMATION_TYPE_DIMMED
-                new AnimationFilter()
-                        .animateDimmed(),
+                new AnimationFilter(),
 
                 // ANIMATION_TYPE_CHANGE_POSITION
                 new AnimationFilter()
@@ -6283,7 +5864,6 @@
                         .animateHeight()
                         .animateTopInset()
                         .animateY()
-                        .animateDimmed()
                         .animateZ()
                         .hasDelays(),
 
@@ -6339,7 +5919,6 @@
                 // ANIMATION_TYPE_EVERYTHING
                 new AnimationFilter()
                         .animateAlpha()
-                        .animateDimmed()
                         .animateHideSensitive()
                         .animateHeight()
                         .animateTopInset()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 7c13877..3bdd0e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -875,8 +875,6 @@
         mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
         mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
 
-        mScrimController.setScrimBehindChangeRunnable(mView::updateBackgroundDimming);
-
         mLockscreenShadeTransitionController.setStackScroller(this);
 
         mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener);
@@ -1743,13 +1741,6 @@
     }
 
     /**
-     * Set the dimmed state for all of the notification views.
-     */
-    public void setDimmed(boolean dimmed, boolean animate) {
-        mView.setDimmed(dimmed, animate);
-    }
-
-    /**
      * @return the inset during the full shade transition, that needs to be added to the position
      * of the quick settings edge. This is relevant for media, that is transitioning
      * from the keyguard host to the quick settings one.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 1ef9a8f..9b1952b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -369,13 +369,11 @@
     /** Updates the dimmed and hiding sensitive states of the children. */
     private void updateDimmedAndHideSensitive(AmbientState ambientState,
             StackScrollAlgorithmState algorithmState) {
-        boolean dimmed = ambientState.isDimmed();
         boolean hideSensitive = ambientState.isHideSensitive();
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             ExpandableView child = algorithmState.visibleChildren.get(i);
             ExpandableViewState childViewState = child.getViewState();
-            childViewState.dimmed = dimmed;
             childViewState.hideSensitive = hideSensitive;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index f523793..78fc147 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -42,8 +42,10 @@
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
@@ -99,7 +101,9 @@
     private val alternateBouncerToGoneTransitionViewModel:
         AlternateBouncerToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
+    private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
@@ -155,8 +159,8 @@
             .distinctUntilChanged()
             .dumpWhileCollecting("isShadeLocked")
 
-    private val shadeCollapseFadeInComplete = MutableStateFlow(false)
-            .dumpValue("shadeCollapseFadeInComplete")
+    private val shadeCollapseFadeInComplete =
+        MutableStateFlow(false).dumpValue("shadeCollapseFadeInComplete")
 
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         interactor.configurationBasedDimensions
@@ -187,9 +191,8 @@
                     statesForConstrainedNotifications.contains(it)
                 },
                 keyguardTransitionInteractor
-                    .transitionValue(LOCKSCREEN)
-                    .onStart { emit(0f) }
-                    .map { it > 0 }
+                    .isInTransitionWhere { from, to -> from == LOCKSCREEN || to == LOCKSCREEN }
+                    .onStart { emit(false) }
             ) { constrainedNotificationState, transitioningToOrFromLockscreen ->
                 constrainedNotificationState || transitioningToOrFromLockscreen
             }
@@ -362,16 +365,16 @@
 
     private val alphaWhenGoneAndShadeState: Flow<Float> =
         combineTransform(
-            keyguardTransitionInteractor.transitions
-                .map { step -> step.to == GONE && step.transitionState == FINISHED }
-                .distinctUntilChanged(),
-            keyguardInteractor.statusBarState,
-        ) { isGoneTransitionFinished, statusBarState ->
-            if (isGoneTransitionFinished && statusBarState == SHADE) {
-                emit(1f)
+                keyguardTransitionInteractor.transitions
+                    .map { step -> step.to == GONE && step.transitionState == FINISHED }
+                    .distinctUntilChanged(),
+                keyguardInteractor.statusBarState,
+            ) { isGoneTransitionFinished, statusBarState ->
+                if (isGoneTransitionFinished && statusBarState == SHADE) {
+                    emit(1f)
+                }
             }
-        }
-        .dumpWhileCollecting("alphaWhenGoneAndShadeState")
+            .dumpWhileCollecting("alphaWhenGoneAndShadeState")
 
     fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> {
         // All transition view models are mututally exclusive, and safe to merge
@@ -379,7 +382,9 @@
             merge(
                 alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
                 aodToLockscreenTransitionViewModel.notificationAlpha,
+                aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                 dozingToLockscreenTransitionViewModel.lockscreenAlpha,
+                dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                 dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                 goneToAodTransitionViewModel.notificationAlpha,
                 goneToDreamingTransitionViewModel.lockscreenAlpha,
@@ -433,30 +438,35 @@
      * alpha sources.
      */
     val glanceableHubAlpha: Flow<Float> =
-        isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade ->
-            combineTransform(
-                lockscreenToGlanceableHubRunning,
-                glanceableHubToLockscreenRunning,
-                merge(
-                    lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
-                    glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
-                )
-            ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
-                if (isOnGlanceableHubWithoutShade) {
-                    // Notifications should not be visible on the glanceable hub.
-                    // TODO(b/321075734): implement a way to actually set the notifications to gone
-                    //  while on the hub instead of just adjusting alpha
-                    emit(0f)
-                } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) {
-                    emit(alpha)
-                } else {
-                    // Not on the hub and no transitions running, return full visibility so we don't
-                    // block the notifications from showing.
-                    emit(1f)
+        isOnGlanceableHubWithoutShade
+            .flatMapLatest { isOnGlanceableHubWithoutShade ->
+                combineTransform(
+                    lockscreenToGlanceableHubRunning,
+                    glanceableHubToLockscreenRunning,
+                    merge(
+                        lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
+                        glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
+                    )
+                ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
+                    if (isOnGlanceableHubWithoutShade) {
+                        // Notifications should not be visible on the glanceable hub.
+                        // TODO(b/321075734): implement a way to actually set the notifications to
+                        // gone
+                        //  while on the hub instead of just adjusting alpha
+                        emit(0f)
+                    } else if (
+                        lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning
+                    ) {
+                        emit(alpha)
+                    } else {
+                        // Not on the hub and no transitions running, return full visibility so we
+                        // don't
+                        // block the notifications from showing.
+                        emit(1f)
+                    }
                 }
             }
-        }
-        .dumpWhileCollecting("glanceableHubAlpha")
+            .dumpWhileCollecting("glanceableHubAlpha")
 
     /**
      * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
@@ -464,20 +474,20 @@
      */
     fun translationY(params: BurnInParameters): Flow<Float> {
         return combine(
-            aodBurnInViewModel.translationY(params).onStart { emit(0f) },
-            isOnLockscreenWithoutShade,
-            merge(
-                keyguardInteractor.keyguardTranslationY,
-                occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
-            )
-        ) { burnInY, isOnLockscreenWithoutShade, translationY ->
-            if (isOnLockscreenWithoutShade) {
-                burnInY + translationY
-            } else {
-                0f
+                aodBurnInViewModel.translationY(params).onStart { emit(0f) },
+                isOnLockscreenWithoutShade,
+                merge(
+                    keyguardInteractor.keyguardTranslationY,
+                    occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
+                )
+            ) { burnInY, isOnLockscreenWithoutShade, translationY ->
+                if (isOnLockscreenWithoutShade) {
+                    burnInY + translationY
+                } else {
+                    0f
+                }
             }
-        }
-        .dumpWhileCollecting("translationY")
+            .dumpWhileCollecting("translationY")
     }
 
     /**
@@ -486,10 +496,10 @@
      */
     val translationX: Flow<Float> =
         merge(
-            lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
-            glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
-        )
-        .dumpWhileCollecting("translationX")
+                lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
+                glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
+            )
+            .dumpWhileCollecting("translationX")
 
     /**
      * When on keyguard, there is limited space to display notifications so calculate how many could
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 560d5ba..48d3157 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -315,8 +315,7 @@
                 mMetricsLogger.count("panel_open", 1);
             } else if (!mQsController.getExpanded()
                     && !mShadeViewController.isExpandingOrCollapsing()) {
-                mQsController.flingQs(0 /* velocity */,
-                        ShadeViewController.FLING_EXPAND);
+                mShadeController.animateExpandQs();
                 mMetricsLogger.count("panel_open_qs", 1);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index d2e36b8..088f894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -207,8 +207,6 @@
     private ScrimView mNotificationsScrim;
     private ScrimView mScrimBehind;
 
-    private Runnable mScrimBehindChangeRunnable;
-
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final DozeParameters mDozeParameters;
@@ -415,11 +413,6 @@
         behindScrim.enableBottomEdgeConcave(mClipsQsScrim);
         mNotificationsScrim.enableRoundedCorners(true);
 
-        if (mScrimBehindChangeRunnable != null) {
-            mScrimBehind.setChangeRunnable(mScrimBehindChangeRunnable, mMainExecutor);
-            mScrimBehindChangeRunnable = null;
-        }
-
         final ScrimState[] states = ScrimState.values();
         for (int i = 0; i < states.length; i++) {
             states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager);
@@ -1542,16 +1535,6 @@
         mScrimBehind.postOnAnimationDelayed(callback, 32 /* delayMillis */);
     }
 
-    public void setScrimBehindChangeRunnable(Runnable changeRunnable) {
-        // TODO: remove this. This is necessary because of an order-of-operations limitation.
-        // The fix is to move more of these class into @SysUISingleton.
-        if (mScrimBehind == null) {
-            mScrimBehindChangeRunnable = changeRunnable;
-        } else {
-            mScrimBehind.setChangeRunnable(changeRunnable, mMainExecutor);
-        }
-    }
-
     private void updateThemeColors() {
         if (mScrimBehind == null) return;
         int background = Utils.getColorAttr(mScrimBehind.getContext(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index ee84434..ba89d4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -95,6 +95,7 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewManagerInteractor;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.unfold.FoldAodAnimationController;
@@ -351,8 +352,8 @@
     private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor;
     private Lazy<KeyguardSurfaceBehindInteractor> mSurfaceBehindInteractor;
     private Lazy<KeyguardDismissActionInteractor> mKeyguardDismissActionInteractor;
-
     private final JavaAdapter mJavaAdapter;
+    private StatusBarKeyguardViewManagerInteractor mStatusBarKeyguardViewManagerInteractor;
 
     @Inject
     public StatusBarKeyguardViewManager(
@@ -386,7 +387,8 @@
             SelectedUserInteractor selectedUserInteractor,
             Lazy<KeyguardSurfaceBehindInteractor> surfaceBehindInteractor,
             JavaAdapter javaAdapter,
-            Lazy<SceneInteractor> sceneInteractorLazy
+            Lazy<SceneInteractor> sceneInteractorLazy,
+            StatusBarKeyguardViewManagerInteractor statusBarKeyguardViewManagerInteractor
     ) {
         mContext = context;
         mViewMediatorCallback = callback;
@@ -421,6 +423,7 @@
         mSurfaceBehindInteractor = surfaceBehindInteractor;
         mJavaAdapter = javaAdapter;
         mSceneInteractorLazy = sceneInteractorLazy;
+        mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor;
     }
 
     KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -503,6 +506,11 @@
                                     lockscreenVis || animatingSurface
                     ),
                     this::consumeShowStatusBarKeyguardView);
+
+            mJavaAdapter.alwaysCollectFlow(
+                    mStatusBarKeyguardViewManagerInteractor.getKeyguardViewOcclusionState(),
+                    (occlusionState) -> setOccluded(
+                            occlusionState.getOccluded(), occlusionState.getAnimate()));
         }
     }
 
@@ -1453,6 +1461,10 @@
             hideAlternateBouncer(false);
             executeAfterKeyguardGoneAction();
         }
+
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            mKeyguardTransitionInteractor.startDismissKeyguardTransition();
+        }
     }
 
     /** Display security message to relevant KeyguardMessageArea. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt
index 71e25e9..541ac48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialog.kt
@@ -23,6 +23,9 @@
 import android.os.Bundle
 import android.view.Gravity
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.WindowInsets
+import android.view.WindowInsets.Type.InsetsType
+import android.view.WindowInsetsAnimation
 import android.view.WindowManager.LayoutParams.MATCH_PARENT
 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
 import android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
@@ -70,16 +73,43 @@
     override fun onStart() {
         super.onStart()
         configurationController?.addCallback(onConfigChanged)
+        window?.decorView?.setWindowInsetsAnimationCallback(insetsAnimationCallback)
     }
 
     override fun onStop() {
         super.onStop()
         configurationController?.removeCallback(onConfigChanged)
+        window?.decorView?.setWindowInsetsAnimationCallback(null)
     }
 
+    /** Called after any insets change. */
+    open fun onInsetsChanged(@InsetsType changedTypes: Int, insets: WindowInsets) {}
+
     /** Can be overridden by subclasses to receive config changed events. */
     open fun onConfigurationChanged() {}
 
+    private val insetsAnimationCallback =
+        object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
+
+            private var lastInsets: WindowInsets? = null
+
+            override fun onEnd(animation: WindowInsetsAnimation) {
+                lastInsets?.let { onInsetsChanged(animation.typeMask, it) }
+            }
+
+            override fun onProgress(
+                insets: WindowInsets,
+                animations: MutableList<WindowInsetsAnimation>,
+            ): WindowInsets {
+                lastInsets = insets
+                onInsetsChanged(changedTypes = allAnimationMasks(animations), insets)
+                return insets
+            }
+
+            private fun allAnimationMasks(animations: List<WindowInsetsAnimation>): Int =
+                animations.fold(0) { acc: Int, it -> acc or it.typeMask }
+        }
+
     private val onConfigChanged =
         object : ConfigurationListener {
             override fun onConfigChanged(newConfig: Configuration?) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index f12a09b..82d9fc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -93,7 +93,7 @@
 
     /**
      * @deprecated Don't subclass SystemUIDialog. Please subclass {@link Delegate} and pass it to
-     *             {@link Factory#create(DialogDelegate)} to create a custom dialog.
+     *             {@link Factory#create(Delegate)} to create a custom dialog.
      */
     @Deprecated
     public SystemUIDialog(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 44c684c..b5efc44 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -379,7 +379,9 @@
                             + " was received. Deferring... Managed profile? " + isManagedProfile);
                     return;
                 }
-                if (android.os.Flags.allowPrivateProfile() && isPrivateProfile(newUserHandle)) {
+                if (android.os.Flags.allowPrivateProfile()
+                        && android.multiuser.Flags.enablePrivateSpaceFeatures()
+                        && isPrivateProfile(newUserHandle)) {
                     mDeferredThemeEvaluation = true;
                     Log.i(TAG, "Deferring theme for private profile till user setup is complete");
                     return;
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
index d19a336..5c53ff9 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.unfold
 
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.annotation.BinderThread
 import android.content.Context
@@ -25,6 +23,7 @@
 import android.os.SystemProperties
 import android.util.Log
 import android.view.animation.DecelerateInterpolator
+import androidx.core.animation.addListener
 import com.android.internal.foldables.FoldLockSettingAvailabilityProvider
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.display.data.repository.DeviceStateRepository
@@ -37,25 +36,17 @@
 import com.android.systemui.unfold.dagger.UnfoldBg
 import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
 import javax.inject.Inject
-import kotlin.coroutines.resume
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withTimeout
 
-@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 class FoldLightRevealOverlayAnimation
 @Inject
 constructor(
@@ -70,9 +61,6 @@
 
     private val revealProgressValueAnimator: ValueAnimator =
         ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT)
-    private val areAnimationEnabled: Flow<Boolean>
-        get() = animationStatusRepository.areAnimationsEnabled()
-
     private lateinit var controller: FullscreenLightRevealAnimationController
     @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null
 
@@ -101,30 +89,33 @@
 
         applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
             deviceStateRepository.state
-                .map { it == DeviceStateRepository.DeviceState.FOLDED }
+                .map { it != DeviceStateRepository.DeviceState.FOLDED }
                 .distinctUntilChanged()
-                .flatMapLatest { isFolded ->
-                    flow<Nothing> {
-                            if (!areAnimationEnabled.first() || !isFolded) {
-                                return@flow
-                            }
-                            withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) {
-                                readyCallback = CompletableDeferred()
-                                val onReady = readyCallback?.await()
-                                readyCallback = null
-                                controller.addOverlay(ALPHA_OPAQUE, onReady)
-                                waitForScreenTurnedOn()
-                            }
+                .filter { isUnfolded -> isUnfolded }
+                .collect { controller.ensureOverlayRemoved() }
+        }
+
+        applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+            deviceStateRepository.state
+                .filter {
+                    animationStatusRepository.areAnimationsEnabled().first() &&
+                        it == DeviceStateRepository.DeviceState.FOLDED
+                }
+                .collect {
+                    try {
+                        withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) {
+                            readyCallback = CompletableDeferred()
+                            val onReady = readyCallback?.await()
+                            readyCallback = null
+                            controller.addOverlay(ALPHA_OPAQUE, onReady)
+                            waitForScreenTurnedOn()
                             playFoldLightRevealOverlayAnimation()
                         }
-                        .catchTimeoutAndLog()
-                        .onCompletion {
-                            val onReady = readyCallback?.takeIf { it.isCompleted }?.getCompleted()
-                            onReady?.run()
-                            readyCallback = null
-                        }
+                    } catch (e: TimeoutCancellationException) {
+                        Log.e(TAG, "Fold light reveal animation timed out")
+                        ensureOverlayRemovedInternal()
+                    }
                 }
-                .collect {}
         }
     }
 
@@ -137,34 +128,19 @@
         powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first()
     }
 
-    private suspend fun playFoldLightRevealOverlayAnimation() {
+    private fun ensureOverlayRemovedInternal() {
+        revealProgressValueAnimator.cancel()
+        controller.ensureOverlayRemoved()
+    }
+
+    private fun playFoldLightRevealOverlayAnimation() {
         revealProgressValueAnimator.duration = ANIMATION_DURATION
         revealProgressValueAnimator.interpolator = DecelerateInterpolator()
         revealProgressValueAnimator.addUpdateListener { animation ->
             controller.updateRevealAmount(animation.animatedFraction)
         }
-        revealProgressValueAnimator.startAndAwaitCompletion()
-    }
-
-    private suspend fun ValueAnimator.startAndAwaitCompletion(): Unit =
-        suspendCancellableCoroutine { continuation ->
-            val listener =
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        continuation.resume(Unit)
-                        removeListener(this)
-                    }
-                }
-            addListener(listener)
-            continuation.invokeOnCancellation { removeListener(listener) }
-            start()
-        }
-
-    private fun <T> Flow<T>.catchTimeoutAndLog() = catch { exception ->
-        when (exception) {
-            is TimeoutCancellationException -> Log.e(TAG, "Fold light reveal animation timed out")
-            else -> throw exception
-        }
+        revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() })
+        revealProgressValueAnimator.start()
     }
 
     private companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
index 0128eb7..80ccd64 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
@@ -22,6 +22,24 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.onStart
 
+fun BatteryController.isDevicePluggedIn(): Flow<Boolean> {
+    return conflatedCallbackFlow {
+            val batteryCallback =
+                object : BatteryController.BatteryStateChangeCallback {
+                    override fun onBatteryLevelChanged(
+                        level: Int,
+                        pluggedIn: Boolean,
+                        charging: Boolean
+                    ) {
+                        trySend(pluggedIn)
+                    }
+                }
+            addCallback(batteryCallback)
+            awaitClose { removeCallback(batteryCallback) }
+        }
+        .onStart { emit(isPluggedIn) }
+}
+
 fun BatteryController.isBatteryPowerSaveEnabled(): Flow<Boolean> {
     return conflatedCallbackFlow {
             val batteryCallback =
@@ -35,3 +53,35 @@
         }
         .onStart { emit(isPowerSave) }
 }
+
+fun BatteryController.getBatteryLevel(): Flow<Int> {
+    return conflatedCallbackFlow {
+            val batteryCallback =
+                object : BatteryController.BatteryStateChangeCallback {
+                    override fun onBatteryLevelChanged(
+                        level: Int,
+                        pluggedIn: Boolean,
+                        charging: Boolean
+                    ) {
+                        trySend(level)
+                    }
+                }
+            addCallback(batteryCallback)
+            awaitClose { removeCallback(batteryCallback) }
+        }
+        .onStart { emit(0) }
+}
+
+fun BatteryController.isExtremePowerSaverEnabled(): Flow<Boolean> {
+    return conflatedCallbackFlow {
+            val batteryCallback =
+                object : BatteryController.BatteryStateChangeCallback {
+                    override fun onExtremeBatterySaverChanged(isExtreme: Boolean) {
+                        trySend(isExtreme)
+                    }
+                }
+            addCallback(batteryCallback)
+            awaitClose { removeCallback(batteryCallback) }
+        }
+        .onStart { emit(isExtremeSaverOn) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
index 0d6c0f5..10cf082 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
@@ -25,8 +25,11 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 
+import com.android.app.tracing.TraceUtils;
 import com.android.systemui.settings.UserTracker;
 
+import kotlin.Unit;
+
 /**
  * Used to interact with per-user Settings.Secure and Settings.System settings (but not
  * Settings.Global, since those do not vary per-user)
@@ -123,8 +126,16 @@
     default void registerContentObserverForUser(
             Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
             int userHandle) {
-        getContentResolver().registerContentObserver(
-                uri, notifyForDescendants, settingsObserver, getRealUserHandle(userHandle));
+        TraceUtils.trace(
+                () -> {
+                    // The limit for trace tags length is 127 chars, which leaves us 90 for Uri.
+                    return "USP#registerObserver#[" + uri.toString() + "]";
+                }, () -> {
+                    getContentResolver().registerContentObserver(
+                            uri, notifyForDescendants, settingsObserver,
+                            getRealUserHandle(userHandle));
+                    return Unit.INSTANCE;
+                });
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 4045630..deec215 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -120,7 +120,7 @@
 import com.android.systemui.haptics.slider.HapticSliderViewBinder;
 import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
 import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
@@ -265,7 +265,7 @@
     private final Object mSafetyWarningLock = new Object();
     private final Accessibility mAccessibility = new Accessibility();
     private final ConfigurationController mConfigurationController;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
     private final CsdWarningDialog.Factory mCsdWarningDialogFactory;
     private final VolumePanelNavigationInteractor mVolumePanelNavigationInteractor;
     private final VolumeNavigator mVolumeNavigator;
@@ -316,7 +316,7 @@
             AccessibilityManagerWrapper accessibilityManagerWrapper,
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             InteractionJankMonitor interactionJankMonitor,
             VolumePanelNavigationInteractor volumePanelNavigationInteractor,
             VolumeNavigator volumeNavigator,
@@ -340,7 +340,7 @@
         mAccessibilityMgr = accessibilityManagerWrapper;
         mDeviceProvisionedController = deviceProvisionedController;
         mConfigurationController = configurationController;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
         mCsdWarningDialogFactory = csdWarningDialogFactory;
         mShowActiveStreamOnly = showActiveStreamOnly();
         mHasSeenODICaptionsTooltip =
@@ -1199,7 +1199,7 @@
             mSettingsIcon.setOnClickListener(v -> {
                 Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
                 dismissH(DISMISS_REASON_SETTINGS_CLICKED);
-                mMediaOutputDialogFactory.dismiss();
+                mMediaOutputDialogManager.dismiss();
                 mVolumeNavigator.openVolumePanel(
                         mVolumePanelNavigationInteractor.getVolumePanelRoute());
             });
@@ -2082,6 +2082,9 @@
                 } else {
                     row.anim.cancel();
                     row.anim.setIntValues(progress, newProgress);
+                    // The animator can't keep up with the volume changes so haptics need to be
+                    // triggered here. This happens when the volume keys are continuously pressed.
+                    row.deliverOnProgressChangedHaptics(false, newProgress);
                 }
                 row.animTargetProgress = newProgress;
                 row.anim.setDuration(UPDATE_ANIMATION_DURATION);
@@ -2486,10 +2489,10 @@
             if (getActiveRow().equals(mRow)
                     && mRow.slider.getVisibility() == VISIBLE
                     && mRow.mHapticPlugin != null) {
-                mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
-                if (!fromUser) {
-                    // Consider a change from program as the volume key being continuously pressed
-                    mRow.mHapticPlugin.onKeyDown();
+                if (fromUser || mRow.animTargetProgress == progress) {
+                    // Deliver user-generated slider changes immediately, or when the animation
+                    // completes
+                    mRow.deliverOnProgressChangedHaptics(fromUser, progress);
                 }
             }
             if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
@@ -2571,11 +2574,11 @@
                 /* progressInterpolatorFactor= */ 1f,
                 /* progressBasedDragMinScale= */ 0f,
                 /* progressBasedDragMaxScale= */ 0.2f,
-                /* additionalVelocityMaxBump= */ 0.15f,
+                /* additionalVelocityMaxBump= */ 0.25f,
                 /* deltaMillisForDragInterval= */ 0f,
-                /* deltaProgressForDragThreshold= */ 0.015f,
-                /* numberOfLowTicks= */ 5,
-                /* maxVelocityToScale= */ 300f,
+                /* deltaProgressForDragThreshold= */ 0.05f,
+                /* numberOfLowTicks= */ 4,
+                /* maxVelocityToScale= */ 200,
                 /* velocityAxis= */ MotionEvent.AXIS_Y,
                 /* upperBookendScale= */ 1f,
                 /* lowerBookendScale= */ 0.05f,
@@ -2642,6 +2645,14 @@
         void removeHaptics() {
             slider.setOnTouchListener(null);
         }
+
+        void deliverOnProgressChangedHaptics(boolean fromUser, int progress) {
+            mHapticPlugin.onProgressChanged(slider, progress, fromUser);
+            if (!fromUser) {
+                // Consider a change from program as the volume key being continuously pressed
+                mHapticPlugin.onKeyDown();
+            }
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 64a5644..1f87ec8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -24,7 +24,7 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -100,7 +100,7 @@
             AccessibilityManagerWrapper accessibilityManagerWrapper,
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             InteractionJankMonitor interactionJankMonitor,
             VolumePanelNavigationInteractor volumePanelNavigationInteractor,
             VolumeNavigator volumeNavigator,
@@ -116,7 +116,7 @@
                 accessibilityManagerWrapper,
                 deviceProvisionedController,
                 configurationController,
-                mediaOutputDialogFactory,
+                mediaOutputDialogManager,
                 interactionJankMonitor,
                 volumePanelNavigationInteractor,
                 volumeNavigator,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index 170b32c..cb16abe 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -16,14 +16,11 @@
 
 package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
 
-import android.content.Intent
-import android.provider.Settings
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
-import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import javax.inject.Inject
@@ -33,29 +30,22 @@
 class MediaOutputActionsInteractor
 @Inject
 constructor(
-    private val mediaOutputDialogFactory: MediaOutputDialogFactory,
-    private val activityStarter: ActivityStarter,
+    private val mediaOutputDialogManager: MediaOutputDialogManager,
 ) {
 
-    fun onDeviceClick(expandable: Expandable) {
-        activityStarter.startActivity(
-            Intent(Settings.ACTION_BLUETOOTH_SETTINGS),
-            true,
-            expandable.activityTransitionController(),
-        )
-    }
-
     fun onBarClick(session: MediaDeviceSession, expandable: Expandable) {
         when (session) {
             is MediaDeviceSession.Active -> {
-                mediaOutputDialogFactory.createWithController(
+                mediaOutputDialogManager.createAndShowWithController(
                     session.packageName,
                     false,
                     expandable.dialogController()
                 )
             }
             is MediaDeviceSession.Inactive -> {
-                mediaOutputDialogFactory.createDialogForSystemRouting(expandable.dialogController())
+                mediaOutputDialogManager.createAndShowForSystemRouting(
+                    expandable.dialogController()
+                )
             }
             else -> {
                 /* do nothing */
@@ -68,7 +58,7 @@
             cuj =
                 DialogCuj(
                     InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                    MediaOutputDialogFactory.INTERACTION_JANK_TAG
+                    MediaOutputDialogManager.INTERACTION_JANK_TAG
                 )
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/DeviceIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/DeviceIconViewModel.kt
index e518ed0..37bf661 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/DeviceIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/DeviceIconViewModel.kt
@@ -26,13 +26,13 @@
     val iconColor: Color
     val backgroundColor: Color
 
-    class IsPlaying(
+    data class IsPlaying(
         override val icon: Icon,
         override val iconColor: Color,
         override val backgroundColor: Color,
     ) : DeviceIconViewModel
 
-    class IsNotPlaying(
+    data class IsNotPlaying(
         override val icon: Icon,
         override val iconColor: Color,
         override val backgroundColor: Color,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index 85d6c9e..37661b5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -113,11 +113,6 @@
     private fun MediaDeviceSession.isPlaying(): Boolean =
         this is MediaDeviceSession.Active && playbackState?.isActive == true
 
-    fun onDeviceClick(expandable: Expandable) {
-        actionsInteractor.onDeviceClick(expandable)
-        volumePanelViewModel.dismissPanel()
-    }
-
     fun onBarClick(expandable: Expandable) {
         actionsInteractor.onBarClick(mediaDeviceSession.value, expandable)
         volumePanelViewModel.dismissPanel()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java
index 90587d7..13fb42c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerBaseTest.java
@@ -16,6 +16,8 @@
 
 package com.android.keyguard;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -30,6 +32,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.power.data.repository.FakePowerRepository;
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory;
 import com.android.systemui.res.R;
@@ -59,6 +62,7 @@
     @Mock protected KeyguardStatusViewController mControllerMock;
     @Mock protected InteractionJankMonitor mInteractionJankMonitor;
     @Mock protected ViewTreeObserver mViewTreeObserver;
+    @Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     @Mock protected DumpManager mDumpManager;
     protected FakeKeyguardRepository mFakeKeyguardRepository;
     protected FakePowerRepository mFakePowerRepository;
@@ -89,6 +93,7 @@
                 mKeyguardLogger,
                 mInteractionJankMonitor,
                 deps.getKeyguardInteractor(),
+                mKeyguardTransitionInteractor,
                 mDumpManager,
                 PowerInteractorFactory.create(
                         mFakePowerRepository
@@ -105,6 +110,7 @@
 
         when(mKeyguardStatusView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
         when(mKeyguardClockSwitchController.getView()).thenReturn(mKeyguardClockSwitch);
+        when(mKeyguardTransitionInteractor.getGoneToAodTransition()).thenReturn(emptyFlow());
         when(mKeyguardStatusView.findViewById(R.id.keyguard_status_area))
                 .thenReturn(mKeyguardStatusAreaView);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
index 24a5e80..2e04007 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
@@ -15,14 +15,11 @@
 package com.android.systemui;
 
 import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
 
 import android.os.Looper;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.statusbar.policy.FlashlightController;
-
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -33,9 +30,9 @@
 
     @Test
     public void testClassDependency() {
-        FlashlightController f = mock(FlashlightController.class);
-        mDependency.injectTestDependency(FlashlightController.class, f);
-        Assert.assertEquals(f, Dependency.get(FlashlightController.class));
+        FakeClass f = new FakeClass();
+        mDependency.injectTestDependency(FakeClass.class, f);
+        Assert.assertEquals(f, Dependency.get(FakeClass.class));
     }
 
     @Test
@@ -53,4 +50,8 @@
         Dependency dependency = initializer.getSysUIComponent().createDependency();
         dependency.start();
     }
+
+    private static class FakeClass {
+
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 3da7261..095c945 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -22,10 +22,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import android.app.ActivityManager;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.display.DisplayManager;
@@ -75,13 +74,13 @@
     private AccessibilityManager mAccessibilityManager;
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private AccessibilityFloatingMenuController mController;
+    @Mock
     private AccessibilityButtonTargetsObserver mTargetsObserver;
+    @Mock
     private AccessibilityButtonModeObserver mModeObserver;
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardCallbackCaptor;
     private KeyguardUpdateMonitorCallback mKeyguardCallback;
-    private int mLastButtonMode;
-    private String mLastButtonTargets;
     @Mock
     private SecureSettings mSecureSettings;
 
@@ -97,10 +96,14 @@
 
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
-        mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
-        mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, UserHandle.USER_CURRENT);
+
+        when(mTargetsObserver.getCurrentAccessibilityButtonTargets())
+                .thenReturn(Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT));
+
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_BUTTON_MODE, UserHandle.USER_CURRENT));
     }
 
     @After
@@ -109,13 +112,6 @@
             mController.onAccessibilityButtonTargetsChanged("");
             mController = null;
         }
-
-        Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastButtonTargets,
-                UserHandle.USER_CURRENT);
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, mLastButtonMode,
-                UserHandle.USER_CURRENT);
     }
 
     @Test
@@ -227,9 +223,8 @@
 
     @Test
     public void onAccessibilityButtonModeChanged_floatingModeAndHasButtonTargets_showWidget() {
-        Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                ActivityManager.getCurrentUser());
+        when(mTargetsObserver.getCurrentAccessibilityButtonTargets())
+                .thenReturn(TEST_A11Y_BTN_TARGETS);
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
@@ -239,8 +234,8 @@
 
     @Test
     public void onAccessibilityButtonModeChanged_floatingModeAndNoButtonTargets_destroyWidget() {
-        Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser());
+        when(mTargetsObserver.getCurrentAccessibilityButtonTargets()).thenReturn("");
+
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
@@ -250,9 +245,8 @@
 
     @Test
     public void onAccessibilityButtonModeChanged_navBarModeAndHasButtonTargets_destroyWidget() {
-        Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                ActivityManager.getCurrentUser());
+        when(mTargetsObserver.getCurrentAccessibilityButtonTargets())
+                .thenReturn(TEST_A11Y_BTN_TARGETS);
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
@@ -262,8 +256,7 @@
 
     @Test
     public void onAccessibilityButtonModeChanged_navBarModeAndNoButtonTargets_destroyWidget() {
-        Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser());
+        when(mTargetsObserver.getCurrentAccessibilityButtonTargets()).thenReturn("");
         mController = setUpController();
 
         mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
@@ -273,9 +266,8 @@
 
     @Test
     public void onAccessibilityButtonTargetsChanged_floatingModeAndHasButtonTargets_showWidget() {
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                ActivityManager.getCurrentUser());
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -285,9 +277,8 @@
 
     @Test
     public void onAccessibilityButtonTargetsChanged_floatingModeAndNoButtonTargets_destroyWidget() {
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                ActivityManager.getCurrentUser());
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged("");
@@ -297,9 +288,8 @@
 
     @Test
     public void onAccessibilityButtonTargetsChanged_navBarModeAndHasButtonTargets_destroyWidget() {
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
-                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser());
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -309,9 +299,8 @@
 
     @Test
     public void onAccessibilityButtonTargetsChanged_navBarModeAndNoButtonTargets_destroyWidget() {
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
-                ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser());
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
         mController = setUpController();
 
         mController.onAccessibilityButtonTargetsChanged("");
@@ -321,9 +310,8 @@
 
     @Test
     public void onTargetsChanged_isFloatingViewLayerControllerCreated() {
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                UserHandle.USER_CURRENT);
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
 
         mController = setUpController();
         mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -335,8 +323,6 @@
         final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
         final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
         final FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext);
-        mTargetsObserver = spy(Dependency.get(AccessibilityButtonTargetsObserver.class));
-        mModeObserver = spy(Dependency.get(AccessibilityButtonModeObserver.class));
         mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
         final AccessibilityFloatingMenuController controller =
                 new AccessibilityFloatingMenuController(mContextWrapper, windowManager,
@@ -348,12 +334,11 @@
     }
 
     private void enableAccessibilityFloatingMenuConfig() {
-        Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
-                ActivityManager.getCurrentUser());
-        Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
-                ActivityManager.getCurrentUser());
+        when(mTargetsObserver.getCurrentAccessibilityButtonTargets())
+                .thenReturn(TEST_A11Y_BTN_TARGETS);
+
+        when(mModeObserver.getCurrentAccessibilityButtonMode())
+                .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
     }
 
     private void captureKeyguardUpdateMonitorCallback() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
index f07932c..e3be3822 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.core.animation.doOnEnd
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.doOnEnd
@@ -30,6 +31,7 @@
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 @RunWithLooper
+@FlakyTest(bugId = 302149604)
 class AnimatorTestRuleOrderTest : SysuiTestCase() {
 
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index 043dcaa..3c073d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -193,6 +193,34 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
+    fun modeEstimate_batteryPercentView_isNotNull_flagOn() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        // New battery icon only uses the percent view for the estimate text
+        assertThat(mBatteryMeterView.batteryPercentView).isNotNull()
+        // Make sure that it was added to the view hierarchy
+        assertThat(mBatteryMeterView.batteryPercentView.parent).isNotNull()
+    }
+
+    @Test
+    @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
+    fun modePercent_batteryPercentView_isNull_flagOn() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        // New battery icon only uses the percent view for the estimate text
+        assertThat(mBatteryMeterView.batteryPercentView).isNull()
+    }
+
+    @Test
     fun contentDescription_manyUpdates_alwaysUpdated() {
         // BatteryDefender
         mBatteryMeterView.onBatteryLevelChanged(90, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index 7a18477..a569cee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -42,7 +42,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -77,7 +77,8 @@
     @Mock SysUiState mSysUiState;
     @Mock
     DialogTransitionAnimator mDialogTransitionAnimator;
-    @Mock MediaOutputDialogFactory mMediaOutputDialogFactory;
+    @Mock
+    MediaOutputDialogManager mMediaOutputDialogManager;
     private SystemUIDialog mDialog;
     private TextView mTitle;
     private TextView mSubTitle;
@@ -96,7 +97,7 @@
 
         mBroadcastDialogDelegate = new BroadcastDialogDelegate(
                 mContext,
-                mMediaOutputDialogFactory,
+                mMediaOutputDialogManager,
                 mLocalBluetoothManager,
                 new UiEventLoggerFake(),
                 mFakeExecutor,
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 b25fb6e..30519b0 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
@@ -16,14 +16,17 @@
 
 package com.android.systemui.display.ui.view
 
+import android.graphics.Insets
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
+import android.view.WindowInsets
 import androidx.test.filters.SmallTest
 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 com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -41,6 +44,7 @@
 
     private val onStartMirroringCallback = mock<View.OnClickListener>()
     private val onCancelCallback = mock<View.OnClickListener>()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -96,10 +100,40 @@
         verify(onStartMirroringCallback).onClick(any())
     }
 
+    @Test
+    fun onInsetsChanged_navBarInsets_updatesBottomPadding() {
+        dialog.show()
+
+        val insets = buildInsets(WindowInsets.Type.navigationBars(), TEST_BOTTOM_INSETS)
+        dialog.onInsetsChanged(WindowInsets.Type.navigationBars(), insets)
+
+        assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom)
+            .isEqualTo(TEST_BOTTOM_INSETS)
+    }
+
+    @Test
+    fun onInsetsChanged_otherType_doesNotUpdateBottomPadding() {
+        dialog.show()
+
+        val insets = buildInsets(WindowInsets.Type.ime(), TEST_BOTTOM_INSETS)
+        dialog.onInsetsChanged(WindowInsets.Type.ime(), insets)
+
+        assertThat(dialog.requireViewById<View>(R.id.cd_bottom_sheet).paddingBottom)
+            .isNotEqualTo(TEST_BOTTOM_INSETS)
+    }
+
+    private fun buildInsets(@WindowInsets.Type.InsetsType type: Int, bottom: Int): WindowInsets {
+        return WindowInsets.Builder().setInsets(type, Insets.of(0, 0, 0, bottom)).build()
+    }
+
     @After
     fun teardown() {
         if (::dialog.isInitialized) {
             dialog.dismiss()
         }
     }
+
+    private companion object {
+        const val TEST_BOTTOM_INSETS = 1000 // arbitrarily high number
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index 915522d..1a6da76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -53,9 +53,11 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -101,6 +103,8 @@
     private lateinit var underTest: CustomizationProvider
     private lateinit var testScope: TestScope
 
+    private val kosmos = testKosmos()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -185,6 +189,7 @@
                                 },
                         )
                         .keyguardInteractor,
+                shadeInteractor = kosmos.shadeInteractor,
                 lockPatternUtils = lockPatternUtils,
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 489665c..51828c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -6,6 +6,7 @@
 import android.graphics.Point
 import android.graphics.Rect
 import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.RemoteAnimationTarget
@@ -15,6 +16,7 @@
 import android.view.ViewRootImpl
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardViewController
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
@@ -130,6 +132,7 @@
      * surface, or the user will see the wallpaper briefly as the app animates in.
      */
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun noSurfaceAnimation_ifWakeAndUnlocking() {
         whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
 
@@ -320,6 +323,7 @@
      * If we are not wake and unlocking, we expect the unlock animation to play normally.
      */
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun surfaceAnimation_multipleTargets() {
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                 arrayOf(remoteTarget1, remoteTarget2),
@@ -358,6 +362,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun surfaceBehindAlphaOverriddenTo0_ifNotInteractive() {
         whenever(powerManager.isInteractive).thenReturn(false)
 
@@ -389,6 +394,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun surfaceBehindAlphaNotOverriddenTo0_ifInteractive() {
         whenever(powerManager.isInteractive).thenReturn(true)
 
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 0957748..0bd4cbe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -1263,7 +1263,8 @@
                 mSystemPropertiesHelper,
                 () -> mock(WindowManagerLockscreenVisibilityManager.class),
                 mSelectedUserInteractor,
-                mKeyguardInteractor);
+                mKeyguardInteractor,
+                mock(WindowManagerOcclusionManager.class));
         mViewMediator.start();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt
index d0b1dd5..df52265 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractorTest.kt
@@ -122,13 +122,7 @@
         testScope.runTest {
             whenever(burnInHelperWrapper.burnInScale()).thenReturn(0.5f)
 
-            val burnInModel by
-                collectLastValue(
-                    underTest.burnIn(
-                        xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
-                        yDimenResourceId = R.dimen.burn_in_prevention_offset_y
-                    )
-                )
+            val burnInModel by collectLastValue(underTest.keyguardBurnIn)
 
             // After time tick, returns the configured values
             fakeKeyguardRepository.dozeTimeTick(10)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
new file mode 100644
index 0000000..7bef01a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.PowerManager
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import junit.framework.Assert.assertEquals
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromAodTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromAodTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to AOD and set the power interactor asleep.
+        powerInteractor.setAsleepForTest()
+        runBlocking {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope
+            )
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Under default conditions, we should transition to LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() =
+        testScope.runTest {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() =
+        testScope.runTest {
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get all the way to AOD
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should go to OCCLUDED - we came from GONE, but we finished in AOD, so this is no
+            // longer an insecure camera launch and it would be bad if we unlocked now.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're in LOCKSCREEN.
+            assertEquals(
+                KeyguardState.LOCKSCREEN,
+                kosmos.keyguardTransitionInteractor.getFinishedState()
+            )
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testWakeAndUnlock_transitionsToGone_onlyAfterDismissCallPostWakeup() =
+        testScope.runTest {
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking up from wake and unlock should not start any transitions, we'll wait for the
+            // dismiss call.
+            assertThat(transitionRepository).noTransitionsStarted()
+
+            underTest.dismissAod()
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
new file mode 100644
index 0000000..258dbf3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.PowerManager
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import junit.framework.Assert.assertEquals
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromDozingTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromDozingTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to DOZING and set the power interactor asleep.
+        powerInteractor.setAsleepForTest()
+        runBlocking {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope
+            )
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Under default conditions, we should transition to LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() =
+        testScope.runTest {
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Under default conditions, we should transition to LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() =
+        testScope.runTest {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop_evenIfIdleOnCommunal() =
+        testScope.runTest {
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            runCurrent()
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() =
+        testScope.runTest {
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.DOZING,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get all the way to AOD
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.DOZING,
+                testScope = testScope,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should go to OCCLUDED - we came from GONE, but we finished in AOD, so this is no
+            // longer an insecure camera launch and it would be bad if we unlocked now.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're in LOCKSCREEN.
+            assertEquals(
+                KeyguardState.LOCKSCREEN,
+                kosmos.keyguardTransitionInteractor.getFinishedState()
+            )
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
new file mode 100644
index 0000000..f534ba5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromDreamingTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromDreamingTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to DOZING and set the power interactor asleep.
+        powerInteractor.setAsleepForTest()
+        runBlocking {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope
+            )
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_ifDreamEnds_occludingActivityOnTop() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            runCurrent()
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            kosmos.fakeKeyguardRepository.setDreaming(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testDoesNotTransitionToOccluded_occludingActivityOnTop_whileStillDreaming() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            runCurrent()
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            runCurrent()
+
+            assertThat(transitionRepository).noTransitionsStarted()
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionsToLockscreen_whenOccludingActivityEnds() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            runCurrent()
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+}
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 6aebe36..c3e24d5 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
@@ -16,6 +16,8 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.app.ActivityManager
+import android.app.WindowConfiguration
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -26,6 +28,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -42,6 +45,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 
@@ -171,4 +175,49 @@
 
             assertThatRepository(transitionRepository).noTransitionsStarted()
         }
+
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionsToOccluded_whenShowWhenLockedActivityOnTop() =
+        testScope.runTest {
+            underTest.start()
+            runCurrent()
+
+            reset(transitionRepository)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(
+                true,
+                ActivityManager.RunningTaskInfo().apply {
+                    topActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
+                }
+            )
+            runCurrent()
+
+            assertThatRepository(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionsToDream_whenDreamActivityOnTop() =
+        testScope.runTest {
+            underTest.start()
+            runCurrent()
+
+            reset(transitionRepository)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(
+                true,
+                ActivityManager.RunningTaskInfo().apply {
+                    topActivityType = WindowConfiguration.ACTIVITY_TYPE_DREAM
+                }
+            )
+            runCurrent()
+
+            assertThatRepository(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DREAMING,
+                )
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt
new file mode 100644
index 0000000..d3c4848
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromOccludedTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromOccludedTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to OCCLUDED and set up PowerInteractor and the occlusion repository.
+        powerInteractor.setAwakeForTest()
+        runBlocking {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testShowWhenLockedActivity_noLongerOnTop_transitionsToLockscreen() =
+        testScope.runTest {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testShowWhenLockedActivity_noLongerOnTop_transitionsToGlanceableHub_ifIdleOnCommunal() =
+        testScope.runTest {
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            runCurrent()
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
index f33a5c9..7ee8963 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
@@ -16,14 +16,23 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
 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.KeyguardTransitionRepositorySpySubject.Companion.assertThat
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
@@ -31,20 +40,27 @@
 import junit.framework.Assert.assertTrue
 import junit.framework.Assert.fail
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() {
-    val kosmos = testKosmos()
+    val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
     val underTest = kosmos.fromPrimaryBouncerTransitionInteractor
     val testScope = kosmos.testScope
     val selectedUserInteractor = kosmos.selectedUserInteractor
     val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+    val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
 
     @Test
     fun testSurfaceBehindVisibility() =
@@ -193,4 +209,85 @@
                 fail("surfaceBehindModel was unexpectedly null.")
             }
         }
+
+    @Test
+    @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testReturnToLockscreen_whenBouncerHides() =
+        testScope.runTest {
+            underTest.start()
+            bouncerRepository.setPrimaryShow(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            reset(transitionRepository)
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.LOCKSCREEN
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testReturnToGlanceableHub_whenBouncerHides_ifIdleOnCommunal() =
+        testScope.runTest {
+            underTest.start()
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            bouncerRepository.setPrimaryShow(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            reset(transitionRepository)
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GLANCEABLE_HUB
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_bouncerHide_occludingActivityOnTop() =
+        testScope.runTest {
+            underTest.start()
+            bouncerRepository.setPrimaryShow(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            runCurrent()
+
+            // Shouldn't transition to OCCLUDED until the bouncer hides.
+            assertThat(transitionRepository).noTransitionsStarted()
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.OCCLUDED
+                )
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
new file mode 100644
index 0000000..8a77ed2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import kotlin.test.Test
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardOcclusionInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.keyguardOcclusionInteractor
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Test
+    fun testTransitionFromPowerGesture_whileGoingToSleep_isTrue() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertTrue(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testTransitionFromPowerGesture_whileAsleep_isTrue() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertTrue(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testTransitionFromPowerGesture_whileWaking_isFalse() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertFalse(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testTransitionFromPowerGesture_whileAwake_isFalse() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertFalse(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testShowWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() =
+        testScope.runTest {
+            val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture)
+            powerInteractor.setAsleepForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                    true,
+                )
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                    true,
+                    false,
+                )
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                    true,
+                    // Power button gesture was not triggered a second time, so this should remain
+                    // false.
+                    false,
+                )
+        }
+
+    @Test
+    fun testShowWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture)
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+            )
+
+            powerInteractor.setAsleepForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+            )
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index d2a8444..45b2a42 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -46,7 +46,9 @@
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -242,6 +244,8 @@
     private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
     private lateinit var userTracker: UserTracker
 
+    private val kosmos = testKosmos()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -311,6 +315,7 @@
                             featureFlags = featureFlags,
                         )
                         .keyguardInteractor,
+                shadeInteractor = kosmos.shadeInteractor,
                 lockPatternUtils = lockPatternUtils,
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
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 c65a9ef..95606ae 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
@@ -21,8 +21,8 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.systemui.Flags
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -40,7 +40,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
-import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -48,7 +47,6 @@
 import com.android.systemui.shade.data.repository.fakeShadeRepository
 import com.android.systemui.statusbar.commandQueue
 import com.android.systemui.testKosmos
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -92,30 +90,26 @@
     private var commandQueue = kosmos.fakeCommandQueue
     private val shadeRepository = kosmos.fakeShadeRepository
     private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val transitionInteractor = kosmos.keyguardTransitionInteractor
     private lateinit var featureFlags: FakeFeatureFlags
 
     // Used to verify transition requests for test output
     @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
-    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
 
     private val fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor
-    private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
-    private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor
-    private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor
-    private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor
-    private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor
-    private lateinit var fromAlternateBouncerTransitionInteractor:
-        FromAlternateBouncerTransitionInteractor
+    private val fromDreamingTransitionInteractor = kosmos.fromDreamingTransitionInteractor
+    private val fromDozingTransitionInteractor = kosmos.fromDozingTransitionInteractor
+    private val fromOccludedTransitionInteractor = kosmos.fromOccludedTransitionInteractor
+    private val fromGoneTransitionInteractor = kosmos.fromGoneTransitionInteractor
+    private val fromAodTransitionInteractor = kosmos.fromAodTransitionInteractor
+    private val fromAlternateBouncerTransitionInteractor =
+        kosmos.fromAlternateBouncerTransitionInteractor
     private val fromPrimaryBouncerTransitionInteractor =
         kosmos.fromPrimaryBouncerTransitionInteractor
-    private lateinit var fromDreamingLockscreenHostedTransitionInteractor:
-        FromDreamingLockscreenHostedTransitionInteractor
-    private lateinit var fromGlanceableHubTransitionInteractor:
-        FromGlanceableHubTransitionInteractor
+    private val fromDreamingLockscreenHostedTransitionInteractor =
+        kosmos.fromDreamingLockscreenHostedTransitionInteractor
+    private val fromGlanceableHubTransitionInteractor = kosmos.fromGlanceableHubTransitionInteractor
 
     private val powerInteractor = kosmos.powerInteractor
-    private val keyguardInteractor = kosmos.keyguardInteractor
     private val communalInteractor = kosmos.communalInteractor
 
     @Before
@@ -125,122 +119,21 @@
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
         mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+        mSetFlagsRule.disableFlags(
+            Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
+        )
         featureFlags = FakeFeatureFlags()
 
-        val glanceableHubTransitions =
-            GlanceableHubTransitions(
-                bgDispatcher = kosmos.testDispatcher,
-                transitionInteractor = transitionInteractor,
-                transitionRepository = transitionRepository,
-                communalInteractor = communalInteractor
-            )
-
         fromLockscreenTransitionInteractor.start()
         fromPrimaryBouncerTransitionInteractor.start()
-
-        fromDreamingTransitionInteractor =
-            FromDreamingTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    glanceableHubTransitions = glanceableHubTransitions,
-                )
-                .apply { start() }
-
-        fromDreamingLockscreenHostedTransitionInteractor =
-            FromDreamingLockscreenHostedTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                )
-                .apply { start() }
-
-        fromAodTransitionInteractor =
-            FromAodTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                )
-                .apply { start() }
-
-        fromGoneTransitionInteractor =
-            FromGoneTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                    communalInteractor = communalInteractor,
-                )
-                .apply { start() }
-
-        fromDozingTransitionInteractor =
-            FromDozingTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                    communalInteractor = communalInteractor,
-                )
-                .apply { start() }
-
-        fromOccludedTransitionInteractor =
-            FromOccludedTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                    communalInteractor = communalInteractor,
-                )
-                .apply { start() }
-
-        fromAlternateBouncerTransitionInteractor =
-            FromAlternateBouncerTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    communalInteractor = communalInteractor,
-                    powerInteractor = powerInteractor,
-                )
-                .apply { start() }
-
-        fromGlanceableHubTransitionInteractor =
-            FromGlanceableHubTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    glanceableHubTransitions = glanceableHubTransitions,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                )
-                .apply { start() }
-
-        mSetFlagsRule.disableFlags(
-            FLAG_KEYGUARD_WM_STATE_REFACTOR,
-        )
+        fromDreamingTransitionInteractor.start()
+        fromDreamingLockscreenHostedTransitionInteractor.start()
+        fromAodTransitionInteractor.start()
+        fromGoneTransitionInteractor.start()
+        fromDozingTransitionInteractor.start()
+        fromOccludedTransitionInteractor.start()
+        fromAlternateBouncerTransitionInteractor.start()
+        fromGlanceableHubTransitionInteractor.start()
     }
 
     @Test
@@ -257,7 +150,9 @@
                 .startedTransition(
                     to = KeyguardState.PRIMARY_BOUNCER,
                     from = KeyguardState.LOCKSCREEN,
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName =
+                        "FromLockscreenTransitionInteractor" +
+                            "(#listenForLockscreenToPrimaryBouncer)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -282,7 +177,7 @@
                 .startedTransition(
                     to = KeyguardState.DOZING,
                     from = KeyguardState.OCCLUDED,
-                    ownerName = "FromOccludedTransitionInteractor",
+                    ownerName = "FromOccludedTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -307,7 +202,7 @@
                 .startedTransition(
                     to = KeyguardState.AOD,
                     from = KeyguardState.OCCLUDED,
-                    ownerName = "FromOccludedTransitionInteractor",
+                    ownerName = "FromOccludedTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -389,7 +284,7 @@
                 .startedTransition(
                     to = KeyguardState.DOZING,
                     from = KeyguardState.LOCKSCREEN,
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName = "FromLockscreenTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -414,7 +309,7 @@
                 .startedTransition(
                     to = KeyguardState.AOD,
                     from = KeyguardState.LOCKSCREEN,
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName = "FromLockscreenTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -703,6 +598,32 @@
             coroutineContext.cancelChildren()
         }
 
+    /** This handles security method NONE and screen off with lock timeout */
+    @Test
+    fun dreamingToGoneWithKeyguardNotShowing() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to DREAMING
+            keyguardRepository.setDreamingWithOverlay(true)
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING)
+            runCurrent()
+
+            // WHEN the device wakes up without a keyguard
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardRepository.setKeyguardDismissible(true)
+            keyguardRepository.setDreamingWithOverlay(false)
+            advanceTimeBy(60L)
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    to = KeyguardState.GONE,
+                    from = KeyguardState.DREAMING,
+                    ownerName = "FromDreamingTransitionInteractor",
+                    animatorAssertion = { it.isNotNull() }
+                )
+
+            coroutineContext.cancelChildren()
+        }
+
     @Test
     fun dozingToGlanceableHub() =
         testScope.runTest {
@@ -752,7 +673,7 @@
                 .startedTransition(
                     to = KeyguardState.DOZING,
                     from = KeyguardState.GONE,
-                    ownerName = "FromGoneTransitionInteractor",
+                    ownerName = "FromGoneTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -777,7 +698,7 @@
                 .startedTransition(
                     to = KeyguardState.AOD,
                     from = KeyguardState.GONE,
-                    ownerName = "FromGoneTransitionInteractor",
+                    ownerName = "FromGoneTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -1044,12 +965,14 @@
     @Test
     fun primaryBouncerToAod() =
         testScope.runTest {
+            // GIVEN aod available
+            keyguardRepository.setAodAvailable(true)
+            runCurrent()
+
             // GIVEN a prior transition has run to PRIMARY_BOUNCER
             bouncerRepository.setPrimaryShow(true)
             runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
 
-            // GIVEN aod available and starting to sleep
-            keyguardRepository.setAodAvailable(true)
             powerInteractor.setAsleepForTest()
 
             // WHEN the primaryBouncer stops showing
@@ -1059,7 +982,8 @@
             // THEN a transition to AOD should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromPrimaryBouncerTransitionInteractor",
+                    ownerName =
+                        "FromPrimaryBouncerTransitionInteractor" + "(Sleep transition triggered)",
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.AOD,
                     animatorAssertion = { it.isNotNull() },
@@ -1086,7 +1010,8 @@
             // THEN a transition to DOZING should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromPrimaryBouncerTransitionInteractor",
+                    ownerName =
+                        "FromPrimaryBouncerTransitionInteractor" + "(Sleep transition triggered)",
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.DOZING,
                     animatorAssertion = { it.isNotNull() },
@@ -1616,7 +1541,9 @@
             // THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName =
+                        "FromLockscreenTransitionInteractor" +
+                            "(#listenForLockscreenToPrimaryBouncerDragging)",
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.PRIMARY_BOUNCER,
                     animatorAssertion = { it.isNull() }, // dragging should be manually animated
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 2ec2fe3..7290863 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -54,6 +54,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -218,6 +219,7 @@
                 quickAffordanceInteractor =
                     KeyguardQuickAffordanceInteractor(
                         keyguardInteractor = keyguardInteractor,
+                        shadeInteractor = kosmos.shadeInteractor,
                         lockPatternUtils = lockPatternUtils,
                         keyguardStateController = keyguardStateController,
                         userTracker = userTracker,
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
new file mode 100644
index 0000000..864acfb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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 androidx.test.filters.SmallTest
+import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
+
+    @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
+    @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
+
+    private lateinit var underTest: KeyguardIndicationAreaViewModel
+    private lateinit var repository: FakeKeyguardRepository
+
+    private val startButtonFlow =
+        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+            KeyguardQuickAffordanceViewModel(
+                slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId()
+            )
+        )
+    private val endButtonFlow =
+        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+            KeyguardQuickAffordanceViewModel(
+                slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId()
+            )
+        )
+    private val alphaFlow = MutableStateFlow<Float>(1f)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+
+        whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
+            .thenReturn(RETURNED_BURN_IN_OFFSET)
+
+        val withDeps = KeyguardInteractorFactory.create()
+        val keyguardInteractor = withDeps.keyguardInteractor
+        repository = withDeps.repository
+
+        val bottomAreaViewModel: KeyguardBottomAreaViewModel = mock()
+        whenever(bottomAreaViewModel.startButton).thenReturn(startButtonFlow)
+        whenever(bottomAreaViewModel.endButton).thenReturn(endButtonFlow)
+        whenever(bottomAreaViewModel.alpha).thenReturn(alphaFlow)
+        underTest =
+            KeyguardIndicationAreaViewModel(
+                keyguardInteractor = keyguardInteractor,
+                bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
+                keyguardBottomAreaViewModel = bottomAreaViewModel,
+                burnInHelperWrapper = burnInHelperWrapper,
+                shortcutsCombinedViewModel = shortcutsCombinedViewModel,
+                configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
+            )
+    }
+
+    @Test
+    fun alpha() = runTest {
+        val value = collectLastValue(underTest.alpha)
+
+        assertThat(value()).isEqualTo(1f)
+        alphaFlow.value = 0.1f
+        assertThat(value()).isEqualTo(0.1f)
+        alphaFlow.value = 0.5f
+        assertThat(value()).isEqualTo(0.5f)
+        alphaFlow.value = 0.2f
+        assertThat(value()).isEqualTo(0.2f)
+        alphaFlow.value = 0f
+        assertThat(value()).isEqualTo(0f)
+    }
+
+    @Test
+    fun isIndicationAreaPadded() = runTest {
+        repository.setKeyguardShowing(true)
+        val value = collectLastValue(underTest.isIndicationAreaPadded)
+
+        assertThat(value()).isFalse()
+        startButtonFlow.value = startButtonFlow.value.copy(isVisible = true)
+        assertThat(value()).isTrue()
+        endButtonFlow.value = endButtonFlow.value.copy(isVisible = true)
+        assertThat(value()).isTrue()
+        startButtonFlow.value = startButtonFlow.value.copy(isVisible = false)
+        assertThat(value()).isTrue()
+        endButtonFlow.value = endButtonFlow.value.copy(isVisible = false)
+        assertThat(value()).isFalse()
+    }
+
+    @Test
+    fun indicationAreaTranslationX() = runTest {
+        val value = collectLastValue(underTest.indicationAreaTranslationX)
+
+        assertThat(value()).isEqualTo(0f)
+        repository.setClockPosition(100, 100)
+        assertThat(value()).isEqualTo(100f)
+        repository.setClockPosition(200, 100)
+        assertThat(value()).isEqualTo(200f)
+        repository.setClockPosition(200, 200)
+        assertThat(value()).isEqualTo(200f)
+        repository.setClockPosition(300, 100)
+        assertThat(value()).isEqualTo(300f)
+    }
+
+    @Test
+    fun indicationAreaTranslationY() = runTest {
+        val value = collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
+
+        // Negative 0 - apparently there's a difference in floating point arithmetic - FML
+        assertThat(value()).isEqualTo(-0f)
+        val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
+        assertThat(value()).isEqualTo(expected1)
+        val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
+        assertThat(value()).isEqualTo(expected2)
+        val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
+        assertThat(value()).isEqualTo(expected3)
+        val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
+        assertThat(value()).isEqualTo(expected4)
+    }
+
+    private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
+        repository.setDozeAmount(dozeAmount)
+        return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
+    }
+
+    companion object {
+        private const val DEFAULT_BURN_IN_OFFSET = 5
+        private const val RETURNED_BURN_IN_OFFSET = 3
+    }
+}
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 1f14afa..bcec6109 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
@@ -280,6 +280,7 @@
                 quickAffordanceInteractor =
                     KeyguardQuickAffordanceInteractor(
                         keyguardInteractor = keyguardInteractor,
+                        shadeInteractor = shadeInteractor,
                         lockPatternUtils = lockPatternUtils,
                         keyguardStateController = keyguardStateController,
                         userTracker = userTracker,
@@ -643,7 +644,7 @@
 
             val testConfig =
                 TestConfig(
-                    isVisible = true,
+                    isVisible = false,
                     isClickable = false,
                     icon = mock(),
                     canShowWhileLocked = false,
@@ -673,7 +674,7 @@
 
             val testConfig =
                 TestConfig(
-                    isVisible = true,
+                    isVisible = false,
                     isClickable = false,
                     icon = mock(),
                     canShowWhileLocked = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 2f92afa..83e4d31 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -84,7 +84,7 @@
 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.monet.ColorScheme
 import com.android.systemui.monet.Style
 import com.android.systemui.plugins.ActivityStarter
@@ -160,7 +160,7 @@
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var expandedSet: ConstraintSet
     @Mock private lateinit var collapsedSet: ConstraintSet
-    @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
+    @Mock private lateinit var mediaOutputDialogManager: MediaOutputDialogManager
     @Mock private lateinit var mediaCarouselController: MediaCarouselController
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var transitionParent: ViewGroup
@@ -266,7 +266,7 @@
                     mediaViewController,
                     seekBarViewModel,
                     Lazy { mediaDataManager },
-                    mediaOutputDialogFactory,
+                    mediaOutputDialogManager,
                     mediaCarouselController,
                     falsingManager,
                     clock,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
index 0879884..83def8e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
@@ -42,16 +42,16 @@
 
     private MediaOutputDialogReceiver mMediaOutputDialogReceiver;
 
-    private final MediaOutputDialogFactory mMockMediaOutputDialogFactory =
-            mock(MediaOutputDialogFactory.class);
+    private final MediaOutputDialogManager mMockMediaOutputDialogManager =
+            mock(MediaOutputDialogManager.class);
 
-    private final MediaOutputBroadcastDialogFactory mMockMediaOutputBroadcastDialogFactory =
-            mock(MediaOutputBroadcastDialogFactory.class);
+    private final MediaOutputBroadcastDialogManager mMockMediaOutputBroadcastDialogManager =
+            mock(MediaOutputBroadcastDialogManager.class);
 
     @Before
     public void setup() {
-        mMediaOutputDialogReceiver = new MediaOutputDialogReceiver(mMockMediaOutputDialogFactory,
-                mMockMediaOutputBroadcastDialogFactory);
+        mMediaOutputDialogReceiver = new MediaOutputDialogReceiver(mMockMediaOutputDialogManager,
+                mMockMediaOutputBroadcastDialogManager);
     }
 
     @Test
@@ -60,9 +60,10 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, times(1))
-                .create(getContext().getPackageName(), false, null);
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, times(1))
+                .createAndShow(getContext().getPackageName(), false, null);
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -71,8 +72,9 @@
         intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -80,8 +82,9 @@
         Intent intent = new Intent(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG);
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -92,8 +95,9 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -104,9 +108,9 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, times(1))
-                .create(getContext().getPackageName(), true, null);
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, times(1))
+                .createAndShow(getContext().getPackageName(), true, null);
     }
 
     @Test
@@ -117,8 +121,9 @@
         intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -128,8 +133,9 @@
                 MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG);
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -139,8 +145,9 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -148,7 +155,8 @@
         Intent intent = new Intent("UnKnown Action");
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
index dbfab64..bda0e1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
@@ -21,32 +21,25 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() {
 
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val repo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
+    private val kosmos = taskSwitcherKosmos()
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val testScope = kosmos.testScope
+    private val repo = kosmos.activityTaskManagerTasksRepository
 
     @Test
     fun launchRecentTask_taskIsMovedToForeground() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
index fdd434a..6043ede 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -17,50 +17,35 @@
 package com.android.systemui.mediaprojection.taskswitcher.data.repository
 
 import android.os.Binder
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.view.ContentRecordingSession
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
 import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.mediaProjectionManagerRepository
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
 
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
 
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
 
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val repo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper
-        )
+    private val repo = kosmos.mediaProjectionManagerRepository
 
     @Test
     fun switchProjectedTask_stateIsUpdatedWithNewTask() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
index dfb688b..33e65f26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
@@ -17,55 +17,33 @@
 package com.android.systemui.mediaprojection.taskswitcher.domain.interactor
 
 import android.content.Intent
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createSingleTaskSession
 import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherInteractor
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class TaskSwitchInteractorTest : SysuiTestCase() {
 
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val mediaRepo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
-        )
-
-    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val interactor = kosmos.taskSwitcherInteractor
 
     @Test
     fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
index c4e9393..9382c58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
@@ -18,26 +18,22 @@
 
 import android.app.Notification
 import android.app.NotificationManager
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
-import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
-import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherViewModel
 import com.android.systemui.res.R
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -46,39 +42,16 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {
 
     private val notificationManager = mock<NotificationManager>()
-
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val mediaRepo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
-        )
-
-    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
-    private val viewModel =
-        TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val viewModel = kosmos.taskSwitcherViewModel
 
     private lateinit var coordinator: TaskSwitcherNotificationCoordinator
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
index 5dadf21..a468953 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
@@ -17,60 +17,35 @@
 package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel
 
 import android.content.Intent
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createDisplaySession
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
-import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createSingleTaskSession
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherViewModel
 import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel.Companion.NOTIFICATION_MAX_SHOW_DURATION
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class TaskSwitcherNotificationViewModelTest : SysuiTestCase() {
 
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val mediaRepo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
-        )
-
-    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
-
-    private val viewModel =
-        TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val viewModel = kosmos.taskSwitcherViewModel
 
     @Test
     fun uiState_notProjecting_emitsNotShowing() =
@@ -138,6 +113,41 @@
         }
 
     @Test
+    fun uiState_taskChanged_beforeDelayLimit_stillEmitsShowing() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 1)
+            val foregroundTask = createTask(taskId = 2)
+            val uiState by collectLastValue(viewModel.uiState)
+
+            fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask)
+            fakeMediaProjectionManager.dispatchOnSessionSet(
+                session = createSingleTaskSession(projectedTask.token.asBinder())
+            )
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            testScheduler.advanceTimeBy(NOTIFICATION_MAX_SHOW_DURATION - 1.milliseconds)
+            assertThat(uiState)
+                .isEqualTo(TaskSwitcherNotificationUiState.Showing(projectedTask, foregroundTask))
+        }
+
+    @Test
+    fun uiState_taskChanged_afterDelayLimit_emitsNotShowing() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 1)
+            val foregroundTask = createTask(taskId = 2)
+            val uiState by collectLastValue(viewModel.uiState)
+
+            fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask)
+            fakeMediaProjectionManager.dispatchOnSessionSet(
+                session = createSingleTaskSession(projectedTask.token.asBinder())
+            )
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            testScheduler.advanceTimeBy(NOTIFICATION_MAX_SHOW_DURATION)
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    @Test
     fun uiState_projectingTask_foregroundTaskChanged_thenTaskSwitched_emitsNotShowing() =
         testScope.runTest {
             val projectedTask = createTask(taskId = 1)
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 52859cd..d405df7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
@@ -43,13 +43,11 @@
 
 import android.content.res.Configuration;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
 import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
-import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
@@ -61,7 +59,9 @@
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.FakeSystemClock;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.pip.Pip;
 
@@ -76,7 +76,6 @@
 
 /** atest NavigationBarControllerTest */
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper
 @SmallTest
 public class NavigationBarControllerImplTest extends SysuiTestCase {
 
@@ -88,6 +87,8 @@
     private StaticMockitoSession mMockitoSession;
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
+    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
     @Mock
     private CommandQueue mCommandQueue;
     @Mock
@@ -104,7 +105,7 @@
                         mock(NavigationModeController.class),
                         mock(SysUiState.class),
                         mCommandQueue,
-                        Dependency.get(Dependency.MAIN_HANDLER),
+                        mExecutor,
                         mock(ConfigurationController.class),
                         mock(NavBarHelper.class),
                         mTaskbarDelegate,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 7b285ab..ada93db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.mediaprojection.SessionCreationSource
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate
 import com.android.systemui.model.SysUiState
 import com.android.systemui.qs.tiles.RecordIssueTile
 import com.android.systemui.res.R
@@ -41,18 +42,18 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
-import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
@@ -74,12 +75,16 @@
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var userFileManager: UserFileManager
     @Mock private lateinit var sharedPreferences: SharedPreferences
+    @Mock private lateinit var screenCaptureDisabledDialogDelegate:
+            ScreenCaptureDisabledDialogDelegate
+    @Mock private lateinit var screenCaptureDisabledDialog: SystemUIDialog
 
     @Mock private lateinit var sysuiState: SysUiState
     @Mock private lateinit var systemUIDialogManager: SystemUIDialogManager
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock private lateinit var bgExecutor: Executor
-    @Mock private lateinit var mainExecutor: Executor
+    private val systemClock = FakeSystemClock()
+    private val bgExecutor = FakeExecutor(systemClock)
+    private val mainExecutor = FakeExecutor(systemClock)
     @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
 
     private lateinit var dialog: SystemUIDialog
@@ -92,6 +97,8 @@
         whenever(dprLazy.get()).thenReturn(devicePolicyResolver)
         whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
         whenever(userContextProvider.userContext).thenReturn(mContext)
+        whenever(screenCaptureDisabledDialogDelegate.createDialog())
+                .thenReturn(screenCaptureDisabledDialog)
         whenever(
                 userFileManager.getSharedPreferences(
                     eq(RecordIssueTile.TILE_SPEC),
@@ -124,6 +131,7 @@
                     dprLazy,
                     mediaProjectionMetricsLogger,
                     userFileManager,
+                    screenCaptureDisabledDialogDelegate,
                 ) {
                     latch.countDown()
                 }
@@ -163,13 +171,8 @@
         val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
         screenRecordSwitch.isChecked = true
 
-        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-        verify(bgExecutor).execute(bgCaptor.capture())
-        bgCaptor.value.run()
-
-        val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-        verify(mainExecutor).execute(mainCaptor.capture())
-        mainCaptor.value.run()
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
 
         verify(mediaProjectionMetricsLogger, never())
             .notifyProjectionInitiated(
@@ -192,13 +195,8 @@
         val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
         screenRecordSwitch.isChecked = true
 
-        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-        verify(bgExecutor).execute(bgCaptor.capture())
-        bgCaptor.value.run()
-
-        val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-        verify(mainExecutor).execute(mainCaptor.capture())
-        mainCaptor.value.run()
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
 
         verify(mediaProjectionMetricsLogger)
             .notifyProjectionInitiated(
@@ -219,9 +217,7 @@
         val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
         screenRecordSwitch.isChecked = true
 
-        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-        verify(bgExecutor).execute(bgCaptor.capture())
-        bgCaptor.value.run()
+        bgExecutor.runAllReady()
 
         verify(mediaProjectionMetricsLogger)
             .notifyProjectionInitiated(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 6cbe8c9..b3df12ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -17,13 +17,11 @@
 package com.android.systemui.screenrecord;
 
 import static android.os.Process.myUid;
-
 import static com.google.common.truth.Truth.assertThat;
-
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
-
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -48,10 +46,9 @@
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
 import com.android.systemui.mediaprojection.SessionCreationSource;
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
-import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DialogDelegate;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -84,8 +81,6 @@
     @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
-    private UserContextProvider mUserContextProvider;
-    @Mock
     private ScreenCaptureDevicePolicyResolver mDevicePolicyResolver;
     @Mock
     private DialogTransitionAnimator mDialogTransitionAnimator;
@@ -96,6 +91,22 @@
     @Mock
     private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
 
+    @Mock
+    private ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
+    @Mock
+    private SystemUIDialog mScreenCaptureDisabledDialog;
+    @Mock
+    private ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory;
+    @Mock
+    private ScreenRecordDialogDelegate mScreenRecordDialogDelegate;
+    @Mock
+    private ScreenRecordPermissionDialogDelegate.Factory
+            mScreenRecordPermissionDialogDelegateFactory;
+    @Mock
+    private ScreenRecordPermissionDialogDelegate mScreenRecordPermissionDialogDelegate;
+    @Mock
+    private SystemUIDialog mScreenRecordSystemUIDialog;
+
     private FakeFeatureFlags mFeatureFlags;
     private RecordingController mController;
     private TestSystemUIDialogFactory mDialogFactory;
@@ -108,8 +119,6 @@
         Context spiedContext = spy(mContext);
         when(spiedContext.getUserId()).thenReturn(TEST_USER_ID);
 
-        when(mUserContextProvider.getUserContext()).thenReturn(spiedContext);
-
         mDialogFactory = new TestSystemUIDialogFactory(
                 mContext,
                 Dependency.get(SystemUIDialogManager.class),
@@ -119,16 +128,26 @@
         );
 
         mFeatureFlags = new FakeFeatureFlags();
+        when(mScreenCaptureDisabledDialogDelegate.createDialog())
+                .thenReturn(mScreenCaptureDisabledDialog);
+        when(mScreenRecordDialogFactory.create(any(), any()))
+                .thenReturn(mScreenRecordDialogDelegate);
+        when(mScreenRecordDialogDelegate.createDialog()).thenReturn(mScreenRecordSystemUIDialog);
+        when(mScreenRecordPermissionDialogDelegateFactory.create(any(), any(), anyInt(), any()))
+                .thenReturn(mScreenRecordPermissionDialogDelegate);
+        when(mScreenRecordPermissionDialogDelegate.createDialog())
+                .thenReturn(mScreenRecordSystemUIDialog);
         mController = new RecordingController(
                 mMainExecutor,
                 mBroadcastDispatcher,
-                mContext,
                 mFeatureFlags,
-                mUserContextProvider,
                 () -> mDevicePolicyResolver,
                 mUserTracker,
                 mMediaProjectionMetricsLogger,
-                mDialogFactory);
+                mScreenCaptureDisabledDialogDelegate,
+                mScreenRecordDialogFactory,
+                mScreenRecordPermissionDialogDelegateFactory
+        );
         mController.addCallback(mCallback);
     }
 
@@ -242,8 +261,8 @@
                         mActivityStarter,
                         /* onStartRecordingClicked= */ null);
 
-        assertThat(dialog).isSameInstanceAs(mDialogFactory.mLastCreatedDialog);
-        assertThat(mDialogFactory.mLastDelegate)
+        assertThat(dialog).isSameInstanceAs(mScreenRecordSystemUIDialog);
+        assertThat(mScreenRecordPermissionDialogDelegate)
                 .isInstanceOf(ScreenRecordPermissionDialogDelegate.class);
     }
 
@@ -255,7 +274,7 @@
         Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
                 mDialogTransitionAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
 
-        assertThat(dialog).isInstanceOf(ScreenRecordDialog.class);
+        assertThat(dialog).isEqualTo(mScreenRecordSystemUIDialog);
     }
 
     @Test
@@ -267,7 +286,7 @@
         Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
                 mDialogTransitionAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
 
-        assertThat(dialog).isInstanceOf(ScreenCaptureDisabledDialog.class);
+        assertThat(dialog).isEqualTo(mScreenCaptureDisabledDialog);
     }
 
     @Test
@@ -284,8 +303,8 @@
                         mActivityStarter,
                         /* onStartRecordingClicked= */ null);
 
-        assertThat(dialog).isSameInstanceAs(mDialogFactory.mLastCreatedDialog);
-        assertThat(mDialogFactory.mLastDelegate)
+        assertThat(dialog).isSameInstanceAs(mScreenRecordSystemUIDialog);
+        assertThat(mScreenRecordPermissionDialogDelegate)
                 .isInstanceOf(ScreenRecordPermissionDialogDelegate.class);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
index 90ced92..6e48074 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.settings.UserContextProvider
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
@@ -58,6 +59,7 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() {
 
+    //@Mock private lateinit var dialogFactory: SystemUIDialog.Factory
     @Mock private lateinit var starter: ActivityStarter
     @Mock private lateinit var controller: RecordingController
     @Mock private lateinit var userContextProvider: UserContextProvider
@@ -71,14 +73,17 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
+
         val systemUIDialogFactory =
-            SystemUIDialog.Factory(
-                context,
-                Dependency.get(SystemUIDialogManager::class.java),
-                Dependency.get(SysUiState::class.java),
-                Dependency.get(BroadcastDispatcher::class.java),
-                Dependency.get(DialogTransitionAnimator::class.java),
-            )
+                SystemUIDialog.Factory(
+                        context,
+                        Dependency.get(SystemUIDialogManager::class.java),
+                        Dependency.get(SysUiState::class.java),
+                        Dependency.get(BroadcastDispatcher::class.java),
+                        Dependency.get(DialogTransitionAnimator::class.java),
+                )
+
         val delegate =
             ScreenRecordPermissionDialogDelegate(
                 UserHandle.of(0),
@@ -88,11 +93,9 @@
                 userContextProvider,
                 onStartRecordingClicked,
                 mediaProjectionMetricsLogger,
-                systemUIDialogFactory
+                systemUIDialogFactory,
             )
         dialog = delegate.createDialog()
-        delegate.onCreate(dialog, savedInstanceState = null)
-        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt
index 2f911fff..92c2404 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt
@@ -22,8 +22,10 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import java.lang.IllegalStateException
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -31,12 +33,14 @@
 import org.mockito.Mockito.verify
 
 @SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
 class ScreenshotSoundControllerTest : SysuiTestCase() {
 
     private val soundProvider = mock<ScreenshotSoundProvider>()
     private val mediaPlayer = mock<MediaPlayer>()
     private val bgDispatcher = UnconfinedTestDispatcher()
     private val scope = TestScope(bgDispatcher)
+
     @Before
     fun setup() {
         whenever(soundProvider.getScreenshotSound()).thenReturn(mediaPlayer)
@@ -45,52 +49,59 @@
     @Test
     fun init_soundLoading() {
         createController()
-        bgDispatcher.scheduler.runCurrent()
+        scope.advanceUntilIdle()
 
         verify(soundProvider).getScreenshotSound()
     }
 
     @Test
-    fun init_soundLoadingException_playAndReleaseDoNotThrow() = runTest {
-        whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException())
+    fun init_soundLoadingException_playAndReleaseDoNotThrow() =
+        scope.runTest {
+            whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException())
 
-        val controller = createController()
+            val controller = createController()
 
-        controller.playCameraSound().await()
-        controller.releaseScreenshotSound().await()
+            controller.playScreenshotSound()
+            advanceUntilIdle()
 
-        verify(mediaPlayer, never()).start()
-        verify(mediaPlayer, never()).release()
-    }
+            verify(mediaPlayer, never()).start()
+            verify(mediaPlayer, never()).release()
+        }
 
     @Test
-    fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() = runTest {
-        val controller = createController()
+    fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() =
+        scope.runTest {
+            val controller = createController()
 
-        controller.playCameraSound().await()
+            controller.playScreenshotSound()
+            advanceUntilIdle()
 
-        verify(mediaPlayer).start()
-    }
+            verify(mediaPlayer).start()
+        }
 
     @Test
-    fun playCameraSound_illegalStateException_doesNotThrow() = runTest {
-        whenever(mediaPlayer.start()).thenThrow(IllegalStateException())
+    fun playCameraSound_illegalStateException_doesNotThrow() =
+        scope.runTest {
+            whenever(mediaPlayer.start()).thenThrow(IllegalStateException())
 
-        val controller = createController()
-        controller.playCameraSound().await()
+            val controller = createController()
+            controller.playScreenshotSound()
+            advanceUntilIdle()
 
-        verify(mediaPlayer).start()
-        verify(mediaPlayer).release()
-    }
+            verify(mediaPlayer).start()
+            verify(mediaPlayer).release()
+        }
 
     @Test
-    fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() = runTest {
-        val controller = createController()
+    fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() =
+        scope.runTest {
+            val controller = createController()
 
-        controller.releaseScreenshotSound().await()
+            controller.releaseScreenshotSound()
+            advanceUntilIdle()
 
-        verify(mediaPlayer).release()
-    }
+            verify(mediaPlayer).release()
+        }
 
     private fun createController() =
         ScreenshotSoundControllerImpl(soundProvider, scope, bgDispatcher)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index f8771b2..acbf997 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -287,7 +287,7 @@
     @Mock protected KeyguardMediaController mKeyguardMediaController;
     @Mock protected NavigationModeController mNavigationModeController;
     @Mock protected NavigationBarController mNavigationBarController;
-    @Mock protected QuickSettingsController mQsController;
+    @Mock protected QuickSettingsControllerImpl mQsController;
     @Mock protected ShadeHeaderController mShadeHeaderController;
     @Mock protected ContentResolver mContentResolver;
     @Mock protected TapAgainViewController mTapAgainViewController;
@@ -380,7 +380,7 @@
     protected final ShadeExpansionStateManager mShadeExpansionStateManager =
             new ShadeExpansionStateManager();
 
-    protected QuickSettingsController mQuickSettingsController;
+    protected QuickSettingsControllerImpl mQuickSettingsController;
     @Mock protected Lazy<NotificationPanelViewController> mNotificationPanelViewControllerLazy;
 
     protected FragmentHostManager.FragmentListener mFragmentListener;
@@ -461,6 +461,7 @@
                 mKeyguardLogger,
                 mInteractionJankMonitor,
                 mKeyguardInteractor,
+                mKeyguardTransitionInteractor,
                 mDumpManager,
                 mPowerInteractor));
 
@@ -793,7 +794,7 @@
 
         when(mNotificationPanelViewControllerLazy.get())
                 .thenReturn(mNotificationPanelViewController);
-        mQuickSettingsController = new QuickSettingsController(
+        mQuickSettingsController = new QuickSettingsControllerImpl(
                 mNotificationPanelViewControllerLazy,
                 mView,
                 mQsFrameTranslateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 960fd59..617b25d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -112,7 +112,7 @@
     @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController
-    @Mock private lateinit var quickSettingsController: QuickSettingsController
+    @Mock private lateinit var quickSettingsController: QuickSettingsControllerImpl
     @Mock
     private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController
     @Mock private lateinit var lockIconViewController: LockIconViewController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index 0b49a95..4809a47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -109,7 +109,7 @@
 
 import kotlinx.coroutines.test.TestScope;
 
-public class QuickSettingsControllerBaseTest extends SysuiTestCase {
+public class QuickSettingsControllerImplBaseTest extends SysuiTestCase {
     protected static final float QS_FRAME_START_X = 0f;
     protected static final int QS_FRAME_WIDTH = 1000;
     protected static final int QS_FRAME_TOP = 0;
@@ -119,7 +119,7 @@
     protected static final int DEFAULT_MIN_HEIGHT_SPLIT_SHADE = DEFAULT_HEIGHT;
     protected static final int DEFAULT_MIN_HEIGHT = 300;
 
-    protected QuickSettingsController mQsController;
+    protected QuickSettingsControllerImpl mQsController;
 
     protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
     protected TestScope mTestScope = mKosmos.getTestScope();
@@ -304,7 +304,7 @@
 
         mMainHandler = new Handler(Looper.getMainLooper());
 
-        mQsController = new QuickSettingsController(
+        mQsController = new QuickSettingsControllerImpl(
                 mPanelViewControllerLazy,
                 mPanelView,
                 mQsFrameTranslateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index 997e0e2..b16f412 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
@@ -41,8 +41,8 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.res.R;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,7 +53,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class QuickSettingsControllerTest extends QuickSettingsControllerBaseTest {
+public class QuickSettingsControllerImplTest extends QuickSettingsControllerImplBaseTest {
 
     @Test
     public void testCloseQsSideEffects() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
similarity index 96%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
index cc4a063..2c453a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
@@ -27,7 +27,7 @@
 
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
-class QuickSettingsControllerWithCoroutinesTest : QuickSettingsControllerBaseTest() {
+class QuickSettingsControllerImplWithCoroutinesTest : QuickSettingsControllerImplBaseTest() {
 
     @Test
     fun isExpansionEnabled_dozing_false() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
index a59ba07..eb692eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
@@ -32,9 +32,9 @@
 import android.view.LayoutInflater;
 import android.view.View;
 
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -51,6 +51,7 @@
 import java.util.List;
 import java.util.Map;
 
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -65,7 +66,7 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(MediaOutputDialogFactory.class);
+        mDependency.injectMockDependency(MediaOutputDialogManager.class);
         allowTestableLooperAsMainThread();
         when(mBindStage.getStageParams(any())).thenReturn(new RowContentBindParams());
         mDynamicChildBindController =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index b3fc25c..24195fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
 import com.android.systemui.SysUITestComponent
 import com.android.systemui.SysUITestModule
@@ -238,6 +240,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun animationsEnabled_isTrue_whenKeyguardIsShowing() =
         testComponent.runTest {
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index e78081f..718f998 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -58,7 +58,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -89,6 +89,8 @@
 import com.android.systemui.statusbar.policy.SmartReplyConstants;
 import com.android.systemui.statusbar.policy.SmartReplyStateInflater;
 import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
+import com.android.systemui.util.time.SystemClock;
+import com.android.systemui.util.time.SystemClockImpl;
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.systemui.wmshell.BubblesTestActivity;
 
@@ -136,6 +138,8 @@
     public final Runnable mFutureDismissalRunnable;
     private @InflationFlag int mDefaultInflationFlags;
     private final FakeFeatureFlags mFeatureFlags;
+    private final SystemClock mSystemClock;
+    private final RowInflaterTaskLogger mRowInflaterTaskLogger;
 
     public NotificationTestHelper(
             Context context,
@@ -155,7 +159,7 @@
         dependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
         dependency.injectMockDependency(NotificationMediaManager.class);
         dependency.injectMockDependency(NotificationShadeWindowController.class);
-        dependency.injectMockDependency(MediaOutputDialogFactory.class);
+        dependency.injectMockDependency(MediaOutputDialogManager.class);
         mMockLogger = mock(ExpandableNotificationRowLogger.class);
         mStatusBarStateController = mock(StatusBarStateController.class);
         mKeyguardBypassController = mock(KeyguardBypassController.class);
@@ -199,6 +203,9 @@
         mFutureDismissalRunnable = mock(Runnable.class);
         when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
                 .thenReturn(mFutureDismissalRunnable);
+
+        mSystemClock = new SystemClockImpl();
+        mRowInflaterTaskLogger = mock(RowInflaterTaskLogger.class);
     }
 
     public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) {
@@ -572,7 +579,8 @@
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
         if (com.android.systemui.Flags.notificationRowUserContext()) {
-            inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry));
+            inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
+                    mRowInflaterTaskLogger));
         }
         mRow = (ExpandableNotificationRow) inflater.inflate(
                 R.layout.status_bar_notification_row,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt
new file mode 100644
index 0000000..f88bd7e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.platform.test.annotations.EnableFlags
+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.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationViewFlipperPausing.FLAG_NAME)
+class NotificationViewFlipperViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    val underTest
+        get() = kosmos.notificationViewFlipperViewModel
+
+    @Test
+    fun testIsPaused_falseWhenViewingShade() =
+        kosmos.testScope.runTest {
+            val isPaused by collectLastValue(underTest.isPaused)
+
+            // WHEN shade is open
+            kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            runCurrent()
+
+            // THEN view flippers should NOT be paused
+            assertThat(isPaused).isFalse()
+        }
+
+    @Test
+    fun testIsPaused_trueWhenViewingKeyguard() =
+        kosmos.testScope.runTest {
+            val isPaused by collectLastValue(underTest.isPaused)
+
+            // WHEN on keyguard
+            kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            runCurrent()
+
+            // THEN view flippers should be paused
+            assertThat(isPaused).isTrue()
+        }
+
+    @Test
+    fun testIsPaused_trueWhenStartingToSleep() =
+        kosmos.testScope.runTest {
+            val isPaused by collectLastValue(underTest.isPaused)
+
+            // WHEN shade is open
+            kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            // AND device is starting to go to sleep
+            kosmos.fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP)
+            runCurrent()
+
+            // THEN view flippers should be paused
+            assertThat(isPaused).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 3d75288..4715b33 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -57,39 +57,6 @@
             )
     }
 
-    // region isDimmed
-    @Test
-    fun isDimmed_whenTrue_shouldReturnTrue() {
-        sut.arrangeDimmed(true)
-
-        assertThat(sut.isDimmed).isTrue()
-    }
-
-    @Test
-    fun isDimmed_whenFalse_shouldReturnFalse() {
-        sut.arrangeDimmed(false)
-
-        assertThat(sut.isDimmed).isFalse()
-    }
-
-    @Test
-    fun isDimmed_whenDozeAmountIsEmpty_shouldReturnTrue() {
-        sut.arrangeDimmed(true)
-        sut.dozeAmount = 0f
-
-        assertThat(sut.isDimmed).isTrue()
-    }
-
-    @Test
-    fun isDimmed_whenPulseExpandingIsFalse_shouldReturnTrue() {
-        sut.arrangeDimmed(true)
-        sut.arrangePulseExpanding(false)
-        sut.dozeAmount = 1f // arrangePulseExpanding changes dozeAmount
-
-        assertThat(sut.isDimmed).isTrue()
-    }
-    // endregion
-
     // region pulseHeight
     @Test
     fun pulseHeight_whenValueChanged_shouldCallListener() {
@@ -383,12 +350,6 @@
 }
 
 // region Arrange helper methods.
-private fun AmbientState.arrangeDimmed(value: Boolean) {
-    isDimmed = value
-    dozeAmount = if (value) 0f else 1f
-    arrangePulseExpanding(!value)
-}
-
 private fun AmbientState.arrangePulseExpanding(value: Boolean) {
     if (value) {
         dozeAmount = 1f
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index f326cea..220305c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -38,7 +38,6 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -73,11 +72,11 @@
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.res.R;
-import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.EmptyShadeView;
@@ -97,13 +96,11 @@
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 
 import org.junit.Assert;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -232,6 +229,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address disabled test
     public void testUpdateStackHeight_qsExpansionZero() {
         final float expansionFraction = 0.2f;
         final float overExpansion = 50f;
@@ -307,14 +305,6 @@
     }
 
     @Test
-    public void testNotDimmedOnKeyguard() {
-        when(mBarState.getState()).thenReturn(StatusBarState.SHADE);
-        mStackScroller.setDimmed(true /* dimmed */, false /* animate */);
-        mStackScroller.setDimmed(true /* dimmed */, true /* animate */);
-        assertFalse(mStackScroller.isDimmed());
-    }
-
-    @Test
     public void updateEmptyView_dndSuppressing() {
         when(mEmptyShadeView.willBeGone()).thenReturn(true);
 
@@ -738,6 +728,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address lack of QS Header
     public void testInsideQSHeader_noOffset() {
         ViewGroup qsHeader = mock(ViewGroup.class);
         Rect boundsOnScreen = new Rect(0, 0, 1000, 1000);
@@ -754,6 +745,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address lack of QS Header
     public void testInsideQSHeader_Offset() {
         ViewGroup qsHeader = mock(ViewGroup.class);
         Rect boundsOnScreen = new Rect(100, 100, 1000, 1000);
@@ -773,12 +765,14 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address disabled test
     public void setFractionToShade_recomputesStackHeight() {
         mStackScroller.setFractionToShade(1f);
         verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat());
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address disabled test
     public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() {
         // Given: shade is not closing, scrollY is 0
         mAmbientState.setScrollY(0);
@@ -877,6 +871,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address disabled test
     public void testSplitShade_hasTopOverscroll() {
         mTestableResources
                 .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
@@ -949,6 +944,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER) // TODO(b/312473478): address disabled test
     public void testSetMaxDisplayedNotifications_notifiesListeners() {
         ExpandableView.OnHeightChangedListener listener =
                 mock(ExpandableView.OnHeightChangedListener.class);
@@ -963,9 +959,8 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     public void testDispatchTouchEvent_sceneContainerDisabled() {
-        Assume.assumeFalse(SceneContainerFlag.isEnabled());
-
         MotionEvent event = MotionEvent.obtain(
                 SystemClock.uptimeMillis(),
                 SystemClock.uptimeMillis(),
@@ -981,34 +976,60 @@
     }
 
     @Test
+    @EnableSceneContainer
     public void testDispatchTouchEvent_sceneContainerEnabled() {
-        Assume.assumeTrue(SceneContainerFlag.isEnabled());
         mStackScroller.setIsBeingDragged(true);
 
-        MotionEvent moveEvent = MotionEvent.obtain(
-                SystemClock.uptimeMillis(),
-                SystemClock.uptimeMillis(),
+        long downTime = SystemClock.uptimeMillis() - 100;
+        MotionEvent moveEvent1 = MotionEvent.obtain(
+                /* downTime= */ downTime,
+                /* eventTime= */ SystemClock.uptimeMillis(),
                 MotionEvent.ACTION_MOVE,
-                0,
-                0,
+                101,
+                201,
                 0
         );
-        MotionEvent syntheticDownEvent = moveEvent.copy();
+        MotionEvent syntheticDownEvent = moveEvent1.copy();
         syntheticDownEvent.setAction(MotionEvent.ACTION_DOWN);
-        mStackScroller.dispatchTouchEvent(moveEvent);
+        mStackScroller.dispatchTouchEvent(moveEvent1);
 
-        verify(mStackScrollLayoutController).sendTouchToSceneFramework(argThat(
-                new MotionEventMatcher(syntheticDownEvent)));
+        assertThatMotionEvent(captureTouchSentToSceneFramework()).matches(syntheticDownEvent);
+        assertTrue(mStackScroller.getIsBeingDragged());
+        clearInvocations(mStackScrollLayoutController);
 
-        mStackScroller.dispatchTouchEvent(moveEvent);
+        MotionEvent moveEvent2 = MotionEvent.obtain(
+                /* downTime= */ downTime,
+                /* eventTime= */ SystemClock.uptimeMillis(),
+                MotionEvent.ACTION_MOVE,
+                102,
+                202,
+                0
+        );
 
-        verify(mStackScrollLayoutController).sendTouchToSceneFramework(moveEvent);
+        mStackScroller.dispatchTouchEvent(moveEvent2);
+
+        assertThatMotionEvent(captureTouchSentToSceneFramework()).matches(moveEvent2);
+        assertTrue(mStackScroller.getIsBeingDragged());
+        clearInvocations(mStackScrollLayoutController);
+
+        MotionEvent upEvent = MotionEvent.obtain(
+                /* downTime= */ downTime,
+                /* eventTime= */ SystemClock.uptimeMillis(),
+                MotionEvent.ACTION_UP,
+                103,
+                203,
+                0
+        );
+
+        mStackScroller.dispatchTouchEvent(upEvent);
+
+        assertThatMotionEvent(captureTouchSentToSceneFramework()).matches(upEvent);
+        assertFalse(mStackScroller.getIsBeingDragged());
     }
 
     @Test
-    @EnableFlags(FLAG_SCENE_CONTAINER)
-    public void testDispatchTouchEvent_sceneContainerEnabled_actionUp() {
-        Assume.assumeTrue(SceneContainerFlag.isEnabled());
+    @EnableSceneContainer
+    public void testDispatchTouchEvent_sceneContainerEnabled_ignoresInitialActionUp() {
         mStackScroller.setIsBeingDragged(true);
 
         MotionEvent upEvent = MotionEvent.obtain(
@@ -1019,21 +1040,18 @@
                 0,
                 0
         );
-        MotionEvent syntheticDownEvent = upEvent.copy();
-        syntheticDownEvent.setAction(MotionEvent.ACTION_DOWN);
 
         mStackScroller.dispatchTouchEvent(upEvent);
-
-        verify(mStackScrollLayoutController, atLeastOnce()).sendTouchToSceneFramework(argThat(
-                new MotionEventMatcher(syntheticDownEvent)));
-
-        mStackScroller.dispatchTouchEvent(upEvent);
-
-        verify(mStackScrollLayoutController, atLeastOnce()).sendTouchToSceneFramework(argThat(
-                new MotionEventMatcher(upEvent)));
+        verify(mStackScrollLayoutController, never()).sendTouchToSceneFramework(any());
         assertFalse(mStackScroller.getIsBeingDragged());
     }
 
+    private MotionEvent captureTouchSentToSceneFramework() {
+        ArgumentCaptor<MotionEvent> captor = ArgumentCaptor.forClass(MotionEvent.class);
+        verify(mStackScrollLayoutController).sendTouchToSceneFramework(captor.capture());
+        return captor.getValue();
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
@@ -1081,20 +1099,23 @@
         );
     }
 
-    private static class MotionEventMatcher implements ArgumentMatcher<MotionEvent> {
-        private final MotionEvent mLeftEvent;
+    private MotionEventSubject assertThatMotionEvent(MotionEvent actual) {
+        return new MotionEventSubject(actual);
+    }
 
-        MotionEventMatcher(MotionEvent leftEvent) {
-            mLeftEvent = leftEvent;
+    private static class MotionEventSubject {
+        private final MotionEvent mActual;
+
+        MotionEventSubject(MotionEvent actual) {
+            mActual = actual;
         }
 
-        @Override
-        public boolean matches(MotionEvent right) {
-            return mLeftEvent.getActionMasked() == right.getActionMasked()
-                    && mLeftEvent.getDownTime() == right.getDownTime()
-                    && mLeftEvent.getEventTime() == right.getEventTime()
-                    && mLeftEvent.getX() == right.getX()
-                    && mLeftEvent.getY() == right.getY();
+        public void matches(MotionEvent expected) {
+            assertThat(mActual.getActionMasked()).isEqualTo(expected.getActionMasked());
+            assertThat(mActual.getDownTime()).isEqualTo(expected.getDownTime());
+            assertThat(mActual.getEventTime()).isEqualTo(expected.getEventTime());
+            assertThat(mActual.getX()).isEqualTo(expected.getX());
+            assertThat(mActual.getY()).isEqualTo(expected.getY());
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index f050857..562aa6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -95,6 +95,7 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewManagerInteractor;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -226,7 +227,8 @@
                         mSelectedUserInteractor,
                         () -> mock(KeyguardSurfaceBehindInteractor.class),
                         mock(JavaAdapter.class),
-                        () -> mock(SceneInteractor.class)) {
+                        () -> mock(SceneInteractor.class),
+                        mock(StatusBarKeyguardViewManagerInteractor.class)) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
@@ -736,7 +738,8 @@
                         mSelectedUserInteractor,
                         () -> mock(KeyguardSurfaceBehindInteractor.class),
                         mock(JavaAdapter.class),
-                        () -> mock(SceneInteractor.class)) {
+                        () -> mock(SceneInteractor.class),
+                        mock(StatusBarKeyguardViewManagerInteractor.class)) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index 1dafcc4..b0404a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -15,13 +15,14 @@
 package com.android.systemui.statusbar.phone;
 
 import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE;
 import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK;
 import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index c02583a..ab28a2f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -718,7 +718,8 @@
 
     @Test
     public void onPrivateProfileAdded_ignoresUntilStartComplete() {
-        mSetFlagsRule.enableFlags(FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         reset(mDeviceProvisionedController);
         when(mUserManager.isManagedProfile(anyInt())).thenReturn(false);
         mBroadcastReceiver.getValue().onReceive(null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 3a6324d..d0261ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -69,7 +69,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.res.R;
@@ -130,7 +130,7 @@
     @Mock
     DeviceProvisionedController mDeviceProvisionedController;
     @Mock
-    MediaOutputDialogFactory mMediaOutputDialogFactory;
+    MediaOutputDialogManager mMediaOutputDialogManager;
     @Mock
     InteractionJankMonitor mInteractionJankMonitor;
     @Mock
@@ -196,7 +196,7 @@
                 mAccessibilityMgr,
                 mDeviceProvisionedController,
                 mConfigurationController,
-                mMediaOutputDialogFactory,
+                mMediaOutputDialogManager,
                 mInteractionJankMonitor,
                 mVolumePanelNavigationInteractor,
                 mVolumeNavigator,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt
index c3f677e..d5411ad 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.activityStarter
@@ -40,5 +41,6 @@
             context = mockedContext,
             activityStarter = activityStarter,
             powerInteractor = powerInteractor,
+            keyguardTransitionInteractor = keyguardTransitionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 1e305d6..793e2d7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.graphics.Point
+import com.android.systemui.common.shared.model.Position
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -57,6 +58,9 @@
     private val _bottomAreaAlpha = MutableStateFlow(1f)
     override val bottomAreaAlpha: StateFlow<Float> = _bottomAreaAlpha
 
+    private val _clockPosition = MutableStateFlow(Position(0, 0))
+    override val clockPosition: StateFlow<Position> = _clockPosition
+
     private val _isKeyguardShowing = MutableStateFlow(false)
     override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
 
@@ -145,6 +149,10 @@
         _bottomAreaAlpha.value = alpha
     }
 
+    override fun setClockPosition(x: Int, y: Int) {
+        _clockPosition.value = Position(x, y)
+    }
+
     fun setKeyguardShowing(isShowing: Boolean) {
         _isKeyguardShowing.value = isShowing
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a9a2d91..dcbd577 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.annotation.FloatRange
+import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -65,55 +66,79 @@
     }
 
     /**
-     * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
-     * [runCurrent] after each step.
+     * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step.
+     *
+     * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part
+     * way using [throughTransitionState].
      */
     suspend fun sendTransitionSteps(
         from: KeyguardState,
         to: KeyguardState,
         testScope: TestScope,
+        throughTransitionState: TransitionState = TransitionState.FINISHED,
     ) {
-        sendTransitionSteps(from, to, testScope.testScheduler)
+        sendTransitionSteps(from, to, testScope.testScheduler, throughTransitionState)
     }
 
     /**
-     * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
-     * [runCurrent] after each step.
+     * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step.
+     *
+     * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part
+     * way using [throughTransitionState].
      */
     suspend fun sendTransitionSteps(
         from: KeyguardState,
         to: KeyguardState,
         testScheduler: TestCoroutineScheduler,
+        throughTransitionState: TransitionState = TransitionState.FINISHED,
     ) {
         sendTransitionStep(
-            TransitionStep(
-                transitionState = TransitionState.STARTED,
-                from = from,
-                to = to,
-                value = 0f,
-            )
+            step =
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = from,
+                    to = to,
+                    value = 0f,
+                )
         )
         testScheduler.runCurrent()
 
-        sendTransitionStep(
-            TransitionStep(
-                transitionState = TransitionState.RUNNING,
-                from = from,
-                to = to,
-                value = 0.5f
+        if (
+            throughTransitionState == TransitionState.RUNNING ||
+                throughTransitionState == TransitionState.FINISHED
+        ) {
+            sendTransitionStep(
+                step =
+                    TransitionStep(
+                        transitionState = TransitionState.RUNNING,
+                        from = from,
+                        to = to,
+                        value = 0.5f
+                    )
             )
-        )
-        testScheduler.runCurrent()
+            testScheduler.runCurrent()
+        }
 
-        sendTransitionStep(
-            TransitionStep(
-                transitionState = TransitionState.FINISHED,
-                from = from,
-                to = to,
-                value = 1f,
+        if (throughTransitionState == TransitionState.FINISHED) {
+            sendTransitionStep(
+                step =
+                    TransitionStep(
+                        transitionState = TransitionState.FINISHED,
+                        from = from,
+                        to = to,
+                        value = 1f,
+                    )
             )
+            testScheduler.runCurrent()
+        }
+    }
+
+    suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
+        this.sendTransitionStep(
+            step = step,
+            validateStep = validateStep,
+            ownerName = step.ownerName
         )
-        testScheduler.runCurrent()
     }
 
     /**
@@ -132,7 +157,22 @@
      * If you're testing something involving transitions themselves and are sure you want to send
      * only a FINISHED step, override [validateStep].
      */
-    suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
+    suspend fun sendTransitionStep(
+        from: KeyguardState = KeyguardState.OFF,
+        to: KeyguardState = KeyguardState.OFF,
+        value: Float = 0f,
+        transitionState: TransitionState = TransitionState.FINISHED,
+        ownerName: String = "",
+        step: TransitionStep =
+            TransitionStep(
+                from = from,
+                to = to,
+                value = value,
+                transitionState = transitionState,
+                ownerName = ownerName
+            ),
+        validateStep: Boolean = true
+    ) {
         _transitions.replayCache.last().let { lastStep ->
             if (
                 validateStep &&
@@ -159,7 +199,9 @@
         step: TransitionStep,
         validateStep: Boolean = true
     ): Job {
-        return coroutineScope.launch { sendTransitionStep(step, validateStep) }
+        return coroutineScope.launch {
+            sendTransitionStep(step = step, validateStep = validateStep)
+        }
     }
 
     suspend fun sendTransitionSteps(
@@ -168,12 +210,13 @@
         validateStep: Boolean = true
     ) {
         steps.forEach {
-            sendTransitionStep(it, validateStep = validateStep)
+            sendTransitionStep(step = it, validateStep = validateStep)
             testScope.testScheduler.runCurrent()
         }
     }
 
     override fun startTransition(info: TransitionInfo): UUID? {
+        Log.i("TEST", "Start transition: ", Exception())
         return if (info.animator == null) UUID.randomUUID() else null
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt
index ad8ccb0..4c8bf90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt
@@ -14,12 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.systemui.volume.panel
+package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.mediaOutputDialogFactory
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 
-val Kosmos.mediaOutputActionsInteractor by
-    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogFactory, activityStarter) }
+val Kosmos.keyguardOcclusionRepository by Kosmos.Fixture { KeyguardOcclusionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..530cbed
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+val Kosmos.fromAlternateBouncerTransitionInteractor by
+    Kosmos.Fixture {
+        FromAlternateBouncerTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            communalInteractor = communalInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
index 2477415..bbe37c1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
@@ -18,19 +18,21 @@
 
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 
 val Kosmos.fromAodTransitionInteractor by
     Kosmos.Fixture {
         FromAodTransitionInteractor(
             transitionRepository = fakeKeyguardTransitionRepository,
             transitionInteractor = keyguardTransitionInteractor,
-            scope = testScope,
+            scope = applicationCoroutineScope,
             bgDispatcher = testDispatcher,
             mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..23dcd96
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromDozingTransitionInteractor by
+    Kosmos.Fixture {
+        FromDozingTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            communalInteractor = communalInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..f7a9d59
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromDreamingLockscreenHostedTransitionInteractor by
+    Kosmos.Fixture {
+        FromDreamingLockscreenHostedTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..135644c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromDreamingTransitionInteractor by
+    Kosmos.Fixture {
+        FromDreamingTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            glanceableHubTransitions = glanceableHubTransitions,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..1695327
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromGlanceableHubTransitionInteractor by
+    Kosmos.Fixture {
+        FromGlanceableHubTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+            glanceableHubTransitions = glanceableHubTransitions,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
index 25fc67a..604d9e4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -17,11 +17,13 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 
 val Kosmos.fromGoneTransitionInteractor by
     Kosmos.Fixture {
@@ -34,5 +36,7 @@
             keyguardInteractor = keyguardInteractor,
             powerInteractor = powerInteractor,
             communalInteractor = communalInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+            biometricSettingsRepository = biometricSettingsRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index 3b52676..162fd90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 
 var Kosmos.fromLockscreenTransitionInteractor by
     Kosmos.Fixture {
@@ -38,5 +39,6 @@
             powerInteractor = powerInteractor,
             glanceableHubTransitions = glanceableHubTransitions,
             swipeToDismissInteractor = swipeToDismissInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..fc740a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+val Kosmos.fromOccludedTransitionInteractor by
+    Kosmos.Fixture {
+        FromOccludedTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            powerInteractor = powerInteractor,
+            communalInteractor = communalInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
index 6b76449..98babff 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
 
 var Kosmos.fromPrimaryBouncerTransitionInteractor by
@@ -40,5 +41,6 @@
             keyguardSecurityModel = keyguardSecurityModel,
             selectedUserInteractor = selectedUserInteractor,
             powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index 6df7493..6cc1e8e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -16,19 +16,21 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import dagger.Lazy
 
 val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by
     Kosmos.Fixture {
         KeyguardTransitionInteractor(
             scope = applicationCoroutineScope,
             repository = keyguardTransitionRepository,
-            fromLockscreenTransitionInteractor = Lazy { fromLockscreenTransitionInteractor },
-            fromPrimaryBouncerTransitionInteractor =
-                Lazy { fromPrimaryBouncerTransitionInteractor },
-            fromAodTransitionInteractor = Lazy { fromAodTransitionInteractor },
+            keyguardRepository = keyguardRepository,
+            fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
+            fromPrimaryBouncerTransitionInteractor = { fromPrimaryBouncerTransitionInteractor },
+            fromAodTransitionInteractor = { fromAodTransitionInteractor },
+            fromAlternateBouncerTransitionInteractor = { fromAlternateBouncerTransitionInteractor },
+            fromDozingTransitionInteractor = { fromDozingTransitionInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt
index ad8ccb0..8162520 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt
@@ -14,12 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.volume.panel
+package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.mediaOutputDialogFactory
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-val Kosmos.mediaOutputActionsInteractor by
-    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogFactory, activityStarter) }
+@ExperimentalCoroutinesApi
+val Kosmos.dozingToOccludedTransitionViewModel by
+    Kosmos.Fixture {
+        DozingToOccludedTransitionViewModel(
+            animationFlow = keyguardTransitionAnimationFlow,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index c2300a1e..75e3ac2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 @file:OptIn(ExperimentalCoroutinesApi::class)
 
 package com.android.systemui.keyguard.ui.viewmodel
@@ -41,8 +40,10 @@
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
+        aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
         dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
+        dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt
index e1b1966..e788669 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.media
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.mediaOutputDialogFactory: MediaOutputDialogFactory by Kosmos.Fixture { mock {} }
+var Kosmos.mediaOutputDialogManager: MediaOutputDialogManager by Kosmos.Fixture { mock {} }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt
index 920e5ee..41d2d60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.taskswitcher
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.IActivityTaskManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt
index 28393e8..2b6032c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.taskswitcher
 
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt
new file mode 100644
index 0000000..d344b75
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediaprojection.taskswitcher
+
+import android.os.Handler
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+
+val Kosmos.fakeActivityTaskManager by Kosmos.Fixture { FakeActivityTaskManager() }
+
+val Kosmos.fakeMediaProjectionManager by Kosmos.Fixture { FakeMediaProjectionManager() }
+
+val Kosmos.activityTaskManagerTasksRepository by
+    Kosmos.Fixture {
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = applicationCoroutineScope,
+            backgroundDispatcher = testDispatcher
+        )
+    }
+
+val Kosmos.mediaProjectionManagerRepository by
+    Kosmos.Fixture {
+        MediaProjectionManagerRepository(
+            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
+            handler = Handler.getMain(),
+            applicationScope = applicationCoroutineScope,
+            tasksRepository = activityTaskManagerTasksRepository,
+            backgroundDispatcher = testDispatcher,
+            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+        )
+    }
+
+val Kosmos.taskSwitcherInteractor by
+    Kosmos.Fixture {
+        TaskSwitchInteractor(mediaProjectionManagerRepository, activityTaskManagerTasksRepository)
+    }
+
+val Kosmos.taskSwitcherViewModel by
+    Kosmos.Fixture { TaskSwitcherNotificationViewModel(taskSwitcherInteractor, testDispatcher) }
+
+@OptIn(ExperimentalCoroutinesApi::class)
+fun taskSwitcherKosmos() = Kosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/battery/BatterySaverTileKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/battery/BatterySaverTileKosmos.kt
index ad8ccb0..a2d1d93 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/battery/BatterySaverTileKosmos.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.volume.panel
+package com.android.systemui.qs.tiles.impl.battery
 
+import com.android.systemui.battery.BatterySaverModule
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.mediaOutputDialogFactory
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
+import com.android.systemui.qs.qsEventLogger
 
-val Kosmos.mediaOutputActionsInteractor by
-    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogFactory, activityStarter) }
+val Kosmos.qsBatterySaverTileConfig by
+    Kosmos.Fixture { BatterySaverModule.provideBatterySaverTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
similarity index 62%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
index ad8ccb0..6772ba3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/MediaOutputComponentKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.volume.panel
+package com.android.systemui.qs.tiles.impl.internet
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.mediaOutputDialogFactory
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.connectivity.ConnectivityModule
 
-val Kosmos.mediaOutputActionsInteractor by
-    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogFactory, activityStarter) }
+val Kosmos.qsInternetTileConfig by
+    Kosmos.Fixture { ConnectivityModule.provideInternetTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt
new file mode 100644
index 0000000..d793740
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.keyguardOcclusionInteractor by
+    Kosmos.Fixture {
+        KeyguardOcclusionInteractor(
+            scope = testScope.backgroundScope,
+            repository = keyguardOcclusionRepository,
+            powerInteractor = powerInteractor,
+            transitionInteractor = keyguardTransitionInteractor,
+            keyguardInteractor = keyguardInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt
new file mode 100644
index 0000000..9e34fe8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.domain.interactor
+
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.statusBarKeyguardViewManagerInteractor by
+    Kosmos.Fixture {
+        StatusBarKeyguardViewManagerInteractor(
+            keyguardTransitionInteractor = this.keyguardTransitionInteractor,
+            keyguardOcclusionInteractor = this.keyguardOcclusionInteractor,
+            powerInteractor = this.powerInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt
new file mode 100644
index 0000000..7ffa262
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
+
+val Kosmos.notificationViewFlipperViewModel by Fixture {
+    NotificationViewFlipperViewModel(
+        dumpManager = dumpManager,
+        stackInteractor = notificationStackInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 106e85c..c013664 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -23,7 +23,9 @@
 import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.goneToAodTransitionViewModel
@@ -57,7 +59,9 @@
         communalInteractor = communalInteractor,
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
+        aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
+        dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
         goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
index 5ae033c..d798b3b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
@@ -30,6 +30,8 @@
     private boolean mIsAodPowerSave = false;
     private boolean mWirelessCharging;
     private boolean mPowerSaveMode = false;
+    private boolean mIsPluggedIn = false;
+    private boolean mIsExtremePowerSave = false;
 
     private final List<BatteryStateChangeCallback> mCallbacks = new ArrayList<>();
 
@@ -64,8 +66,35 @@
     }
 
     @Override
+    public boolean isExtremeSaverOn() {
+        return mIsExtremePowerSave;
+    }
+
+    /**
+     * Note: this does not affect the regular power saver. Triggers all callbacks, only on change.
+     */
+    public void setExtremeSaverOn(Boolean extremePowerSave) {
+        if (extremePowerSave == mIsExtremePowerSave) return;
+
+        mIsExtremePowerSave = extremePowerSave;
+        for (BatteryStateChangeCallback callback: mCallbacks) {
+            callback.onExtremeBatterySaverChanged(extremePowerSave);
+        }
+    }
+
+    @Override
     public boolean isPluggedIn() {
-        return false;
+        return mIsPluggedIn;
+    }
+
+    /**
+     * Notifies all registered callbacks
+     */
+    public void setPluggedIn(boolean pluggedIn) {
+        mIsPluggedIn = pluggedIn;
+        for (BatteryStateChangeCallback cb : mCallbacks) {
+            cb.onBatteryLevelChanged(0, pluggedIn, false);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index 3f20df3..3938f77 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -24,8 +24,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.mediaOutputDialogFactory
-import com.android.systemui.plugins.activityStarter
+import com.android.systemui.media.mediaOutputDialogManager
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -43,7 +42,7 @@
     Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
 
 val Kosmos.mediaOutputActionsInteractor by
-    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogFactory, activityStarter) }
+    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) }
 val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
 val Kosmos.mediaOutputInteractor by
     Kosmos.Fixture {
diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
index f31eb44..23e269a 100644
--- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
+++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
@@ -39,7 +39,9 @@
 import android.content.SharedPreferences;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
+import android.graphics.Point;
 import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -49,16 +51,22 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.Xml;
+import android.view.Display;
+import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -102,6 +110,9 @@
     @VisibleForTesting
     static final String WALLPAPER_INFO_STAGE = "wallpaper-info-stage";
 
+    @VisibleForTesting
+    static final String WALLPAPER_BACKUP_DEVICE_INFO_STAGE = "wallpaper-backup-device-info-stage";
+
     static final String EMPTY_SENTINEL = "empty";
     static final String QUOTA_SENTINEL = "quota";
 
@@ -110,6 +121,11 @@
     static final String SYSTEM_GENERATION = "system_gen";
     static final String LOCK_GENERATION = "lock_gen";
 
+    /**
+     * An approximate area threshold to compare device dimension similarity
+     */
+    static final int AREA_THRESHOLD = 50; // TODO (b/327637867): determine appropriate threshold
+
     // If this file exists, it means we exceeded our quota last time
     private File mQuotaFile;
     private boolean mQuotaExceeded;
@@ -121,6 +137,8 @@
     private boolean mSystemHasLiveComponent;
     private boolean mLockHasLiveComponent;
 
+    private DisplayManager mDisplayManager;
+
     @Override
     public void onCreate() {
         if (DEBUG) {
@@ -137,6 +155,8 @@
 
         mBackupManager = new BackupManager(getBaseContext());
         mEventLogger = new WallpaperEventLogger(mBackupManager, /* wallpaperAgent */ this);
+
+        mDisplayManager = getSystemService(DisplayManager.class);
     }
 
     @Override
@@ -175,9 +195,11 @@
             mSystemHasLiveComponent = mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM) != null;
             mLockHasLiveComponent = mWallpaperManager.getWallpaperInfo(FLAG_LOCK) != null;
 
+            // performing backup of each file based on order of importance
             backupWallpaperInfoFile(/* sysOrLockChanged= */ sysChanged || lockChanged, data);
             backupSystemWallpaperFile(sharedPrefs, sysChanged, sysGeneration, data);
             backupLockWallpaperFileIfItExists(sharedPrefs, lockChanged, lockGeneration, data);
+            backupDeviceInfoFile(data);
         } catch (Exception e) {
             Slog.e(TAG, "Unable to back up wallpaper", e);
             mEventLogger.onBackupException(e);
@@ -191,6 +213,54 @@
         }
     }
 
+    /**
+     * This method backs up the device dimension information. The device data will always get
+     * overwritten when triggering a backup
+     */
+    private void backupDeviceInfoFile(FullBackupDataOutput data)
+            throws IOException {
+        final File deviceInfoStage = new File(getFilesDir(), WALLPAPER_BACKUP_DEVICE_INFO_STAGE);
+
+        // save the dimensions of the device with xml formatting
+        Point dimensions = getScreenDimensions();
+        Display smallerDisplay = getSmallerDisplayIfExists();
+        Point secondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
+                new Point(0, 0);
+
+        deviceInfoStage.createNewFile();
+        FileOutputStream fstream = new FileOutputStream(deviceInfoStage, false);
+        TypedXmlSerializer out = Xml.resolveSerializer(fstream);
+        out.startDocument(null, true);
+        out.startTag(null, "dimensions");
+
+        out.startTag(null, "width");
+        out.text(String.valueOf(dimensions.x));
+        out.endTag(null, "width");
+
+        out.startTag(null, "height");
+        out.text(String.valueOf(dimensions.y));
+        out.endTag(null, "height");
+
+        if (smallerDisplay != null) {
+            out.startTag(null, "secondarywidth");
+            out.text(String.valueOf(secondaryDimensions.x));
+            out.endTag(null, "secondarywidth");
+
+            out.startTag(null, "secondaryheight");
+            out.text(String.valueOf(secondaryDimensions.y));
+            out.endTag(null, "secondaryheight");
+        }
+
+        out.endTag(null, "dimensions");
+        out.endDocument();
+        fstream.flush();
+        FileUtils.sync(fstream);
+        fstream.close();
+
+        if (DEBUG) Slog.v(TAG, "Storing device dimension data");
+        backupFile(deviceInfoStage, data);
+    }
+
     private void backupWallpaperInfoFile(boolean sysOrLockChanged, FullBackupDataOutput data)
             throws IOException {
         final ParcelFileDescriptor wallpaperInfoFd = mWallpaperManager.getWallpaperInfoFile();
@@ -364,9 +434,22 @@
         final File infoStage = new File(filesDir, WALLPAPER_INFO_STAGE);
         final File imageStage = new File(filesDir, SYSTEM_WALLPAPER_STAGE);
         final File lockImageStage = new File(filesDir, LOCK_WALLPAPER_STAGE);
+        final File deviceDimensionsStage = new File(filesDir, WALLPAPER_BACKUP_DEVICE_INFO_STAGE);
         boolean lockImageStageExists = lockImageStage.exists();
 
         try {
+            // Parse the device dimensions of the source device and compare with target to
+            // to identify whether we need to skip the remainder of the restore process
+            Pair<Point, Point> sourceDeviceDimensions = parseDeviceDimensions(
+                    deviceDimensionsStage);
+
+            Point targetDeviceDimensions = getScreenDimensions();
+            if (sourceDeviceDimensions != null && targetDeviceDimensions != null
+                    && isSourceDeviceSignificantlySmallerThanTarget(sourceDeviceDimensions.first,
+                    targetDeviceDimensions)) {
+                Slog.d(TAG, "The source device is significantly smaller than target");
+            }
+
             // First parse the live component name so that we know for logging if we care about
             // logging errors with the image restore.
             ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
@@ -400,6 +483,7 @@
             infoStage.delete();
             imageStage.delete();
             lockImageStage.delete();
+            deviceDimensionsStage.delete();
 
             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
             prefs.edit()
@@ -409,6 +493,66 @@
         }
     }
 
+    /**
+     * This method parses the given file for the backed up device dimensions
+     *
+     * @param deviceDimensions the file which holds the device dimensions
+     * @return the backed up device dimensions
+     */
+    private Pair<Point, Point> parseDeviceDimensions(File deviceDimensions) {
+        int width = 0, height = 0, secondaryHeight = 0, secondaryWidth = 0;
+        try {
+            TypedXmlPullParser parser = Xml.resolvePullParser(
+                    new FileInputStream(deviceDimensions));
+
+            while (parser.next() != XmlPullParser.END_TAG) {
+                if (parser.getEventType() != XmlPullParser.START_TAG) {
+                    continue;
+                }
+
+                String name = parser.getName();
+
+                switch (name) {
+                    case "width":
+                        String widthText = readText(parser);
+                        width = Integer.valueOf(widthText);
+                        break;
+
+                    case "height":
+                        String textHeight = readText(parser);
+                        height = Integer.valueOf(textHeight);
+                        break;
+
+                    case "secondarywidth":
+                        String secondaryWidthText = readText(parser);
+                        secondaryWidth = Integer.valueOf(secondaryWidthText);
+                        break;
+
+                    case "secondaryheight":
+                        String secondaryHeightText = readText(parser);
+                        secondaryHeight = Integer.valueOf(secondaryHeightText);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            return new Pair<>(new Point(width, height), new Point(secondaryWidth, secondaryHeight));
+
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static String readText(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        String result = "";
+        if (parser.next() == XmlPullParser.TEXT) {
+            result = parser.getText();
+            parser.nextTag();
+        }
+        return result;
+    }
+
     @VisibleForTesting
     void updateWallpaperComponent(ComponentName wpService, int which)
             throws IOException {
@@ -691,6 +835,94 @@
         };
     }
 
+    /**
+     * This method retrieves the dimensions of the largest display of the device
+     *
+     * @return a @{Point} object that contains the dimensions of the largest display on the device
+     */
+    private Point getScreenDimensions() {
+        Point largetDimensions = null;
+        int maxArea = 0;
+
+        for (Display display : getInternalDisplays()) {
+            Point displaySize = getRealSize(display);
+
+            int width = displaySize.x;
+            int height = displaySize.y;
+            int area = width * height;
+
+            if (area > maxArea) {
+                maxArea = area;
+                largetDimensions = displaySize;
+            }
+        }
+
+        return largetDimensions;
+    }
+
+    private Point getRealSize(Display display) {
+        DisplayInfo displayInfo = new DisplayInfo();
+        display.getDisplayInfo(displayInfo);
+        return new Point(displayInfo.logicalWidth, displayInfo.logicalHeight);
+    }
+
+    /**
+     * This method returns the smaller display on a multi-display device
+     *
+     * @return Display that corresponds to the smaller display on a device or null if ther is only
+     * one Display on a device
+     */
+    private Display getSmallerDisplayIfExists() {
+        List<Display> internalDisplays = getInternalDisplays();
+        Point largestDisplaySize = getScreenDimensions();
+
+        // Find the first non-matching internal display
+        for (Display display : internalDisplays) {
+            Point displaySize = getRealSize(display);
+            if (displaySize.x != largestDisplaySize.x || displaySize.y != largestDisplaySize.y) {
+                return display;
+            }
+        }
+
+        // If no smaller display found, return null, as there is only a single display
+        return null;
+    }
+
+    /**
+     * This method retrieves the collection of Display objects available in the device.
+     * i.e. non-external displays are ignored
+     *
+     * @return list of displays corresponding to each display in the device
+     */
+    private List<Display> getInternalDisplays() {
+        Display[] allDisplays = mDisplayManager.getDisplays(
+                DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+
+        List<Display> internalDisplays = new ArrayList<>();
+        for (Display display : allDisplays) {
+            if (display.getType() == Display.TYPE_INTERNAL) {
+                internalDisplays.add(display);
+            }
+        }
+        return internalDisplays;
+    }
+
+    /**
+     * This method compares the source and target dimensions, and returns true if there is a
+     * significant difference in area between them and the source dimensions are smaller than the
+     * target dimensions.
+     *
+     * @param sourceDimensions is the dimensions of the source device
+     * @param targetDimensions is the dimensions of the target device
+     */
+    @VisibleForTesting
+    boolean isSourceDeviceSignificantlySmallerThanTarget(Point sourceDimensions,
+            Point targetDimensions) {
+        int rawAreaDelta = (targetDimensions.x * targetDimensions.y)
+                - (sourceDimensions.x * sourceDimensions.y);
+        return rawAreaDelta > AREA_THRESHOLD;
+    }
+
     @VisibleForTesting
     boolean isDeviceInRestore() {
         try {
diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
index 3ecdf3f..ec9223c 100644
--- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
+++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
@@ -59,6 +59,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
@@ -840,6 +841,26 @@
         testParseCropHints(testMap);
     }
 
+    @Test
+    public void test_sourceDimensionsAreLargerThanTarget() {
+        // source device is larger than target, expecting to get false
+        Point sourceDimensions = new Point(2208, 1840);
+        Point targetDimensions = new Point(1080, 2092);
+        boolean isSourceSmaller = mWallpaperBackupAgent
+                .isSourceDeviceSignificantlySmallerThanTarget(sourceDimensions, targetDimensions);
+        assertThat(isSourceSmaller).isEqualTo(false);
+    }
+
+    @Test
+    public void test_sourceDimensionsMuchSmallerThanTarget() {
+        // source device is smaller than target, expecting to get true
+        Point sourceDimensions = new Point(1080, 2092);
+        Point targetDimensions = new Point(2208, 1840);
+        boolean isSourceSmaller = mWallpaperBackupAgent
+                .isSourceDeviceSignificantlySmallerThanTarget(sourceDimensions, targetDimensions);
+        assertThat(isSourceSmaller).isEqualTo(true);
+    }
+
     private void testParseCropHints(Map<Integer, Rect> testMap) throws Exception {
         assumeTrue(multiCrop());
         mockRestoredStaticWallpaperFile(testMap);
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
index cc94090..9057d16 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
@@ -42,7 +42,6 @@
     public static final String KEYBOARD_PATHS = "keyboard_paths";
     public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes";
 
-    public static final String VALUE_N_A = "**n/a**";
     public static final String LIBANDROID_RUNTIME_NAME = "android_runtime";
 
     private static String sInitialDir = new File("").getAbsolutePath();
@@ -130,8 +129,6 @@
         }
         setProperty(CORE_NATIVE_CLASSES, jniClasses);
         setProperty(GRAPHICS_NATIVE_CLASSES, "");
-        setProperty(ICU_DATA_PATH, VALUE_N_A);
-        setProperty(KEYBOARD_PATHS, VALUE_N_A);
 
         RavenwoodUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
     }
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index a754ba5..997f3af 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -59,6 +59,13 @@
 }
 
 flag {
+    name: "enable_magnification_one_finger_panning_gesture"
+    namespace: "accessibility"
+    description: "Whether to allow easy-mode (one finger panning gesture) for magnification"
+    bug: "282039824"
+}
+
+flag {
     name: "fix_drag_pointer_when_ending_drag"
     namespace: "accessibility"
     description: "Send the correct pointer id when transitioning from dragging to delegating states."
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 279bd72..6d1ab9f 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -167,7 +167,7 @@
     })
     public @interface OverscrollState {}
 
-    @VisibleForTesting boolean mIsSinglePanningEnabled;
+    @VisibleForTesting final OneFingerPanningSettingsProvider mOneFingerPanningSettingsProvider;
 
     private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper;
 
@@ -201,7 +201,11 @@
                 displayId,
                 fullScreenMagnificationVibrationHelper,
                 /* magnificationLogger= */ null,
-                ViewConfiguration.get(context));
+                ViewConfiguration.get(context),
+                new OneFingerPanningSettingsProvider(
+                        context,
+                        Flags.enableMagnificationOneFingerPanningGesture()
+                ));
     }
 
     /** Constructor for tests. */
@@ -218,7 +222,9 @@
             int displayId,
             FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper,
             MagnificationLogger magnificationLogger,
-            ViewConfiguration viewConfiguration) {
+            ViewConfiguration viewConfiguration,
+            OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider
+    ) {
         super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap,
                 detectShortcutTrigger, trace, callback);
         if (DEBUG_ALL) {
@@ -301,9 +307,7 @@
         mPanningScalingState = new PanningScalingState(context);
         mSinglePanningState = new SinglePanningState(context);
         mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
-        setSinglePanningEnabled(
-                context.getResources()
-                        .getBoolean(R.bool.config_enable_a11y_magnification_single_panning));
+        mOneFingerPanningSettingsProvider = oneFingerPanningSettingsProvider;
         mOverscrollHandler = new OverscrollHandler();
         mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
 
@@ -317,11 +321,6 @@
         transitionTo(mDetectingState);
     }
 
-    @VisibleForTesting
-    void setSinglePanningEnabled(boolean isEnabled) {
-        mIsSinglePanningEnabled = isEnabled;
-    }
-
     @Override
     void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
         if (event.getActionMasked() == ACTION_DOWN) {
@@ -361,6 +360,7 @@
             Slog.i(mLogTag, "onDestroy(); delayed = "
                     + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue));
         }
+        mOneFingerPanningSettingsProvider.unregister();
 
         if (mScreenStateReceiver != null) {
             mScreenStateReceiver.unregister();
@@ -524,7 +524,7 @@
                     && event.getPointerCount() == 2 // includes the pointer currently being released
                     && mPreviousState == mViewportDraggingState) {
                 // if feature flag is enabled, currently only true on watches
-                if (mIsSinglePanningEnabled) {
+                if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) {
                     mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
                     mOverscrollHandler.clearEdgeState();
                 }
@@ -532,7 +532,7 @@
             } else if (action == ACTION_UP || action == ACTION_CANCEL) {
                 onPanningFinished(event);
                 // if feature flag is enabled, currently only true on watches
-                if (mIsSinglePanningEnabled) {
+                if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) {
                     mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
                     mOverscrollHandler.clearEdgeState();
                 }
@@ -611,7 +611,7 @@
             onPan(second);
             mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
                     distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
-            if (mIsSinglePanningEnabled) {
+            if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) {
                 mOverscrollHandler.onScrollStateChanged(first, second);
             }
             return /* event consumed: */ true;
@@ -1000,7 +1000,7 @@
                                 && event.getPointerCount() == 2) {
                             transitionToViewportDraggingStateAndClear(event);
                         } else if (isActivated() && event.getPointerCount() == 2) {
-                            if (mIsSinglePanningEnabled
+                            if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
                                     && overscrollState(event, mFirstPointerDownLocation)
                                     == OVERSCROLL_VERTICAL_EDGE) {
                                 transitionToDelegatingStateAndClear();
@@ -1008,7 +1008,7 @@
                                 //Primary pointer is swiping, so transit to PanningScalingState
                                 transitToPanningScalingStateAndClear();
                             }
-                        } else if (mIsSinglePanningEnabled
+                        } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
                                 && isActivated()
                                 && event.getPointerCount() == 1) {
                             if (overscrollState(event, mFirstPointerDownLocation)
@@ -1255,7 +1255,7 @@
                         if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
                             transitionToViewportDraggingStateAndClear(event);
                         } else if (isActivated() && event.getPointerCount() == 2) {
-                            if (mIsSinglePanningEnabled
+                            if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
                                     && overscrollState(event, mFirstPointerDownLocation)
                                     == OVERSCROLL_VERTICAL_EDGE) {
                                 transitionToDelegatingStateAndClear();
@@ -1263,7 +1263,7 @@
                                 //Primary pointer is swiping, so transit to PanningScalingState
                                 transitToPanningScalingStateAndClear();
                             }
-                        } else if (mIsSinglePanningEnabled
+                        } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
                                 && isActivated()
                                 && event.getPointerCount() == 1) {
                             if (overscrollState(event, mFirstPointerDownLocation)
@@ -1633,7 +1633,8 @@
                 + ", mPreviousState=" + State.nameOf(mPreviousState)
                 + ", mMagnificationController=" + mFullScreenMagnificationController
                 + ", mDisplayId=" + mDisplayId
-                + ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled
+                + ", mIsSinglePanningEnabled="
+                + mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
                 + ", mOverscrollHandler=" + mOverscrollHandler
                 + '}';
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OneFingerPanningSettingsProvider.java b/services/accessibility/java/com/android/server/accessibility/magnification/OneFingerPanningSettingsProvider.java
new file mode 100644
index 0000000..3cdaf98
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/OneFingerPanningSettingsProvider.java
@@ -0,0 +1,104 @@
+/*
+ * 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.server.accessibility.magnification;
+
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provider for secure settings {@link Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED}.
+ */
+public class OneFingerPanningSettingsProvider {
+
+    @VisibleForTesting
+    static final String KEY = Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED;
+    private static final Uri URI = Settings.Secure.getUriFor(KEY);
+    private AtomicBoolean mCached = new AtomicBoolean();
+    @VisibleForTesting
+    ContentObserver mObserver;
+    @VisibleForTesting
+    ContentResolver mContentResolver;
+
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface State {
+        int OFF = 0;
+        int ON = 1;
+    }
+
+    public OneFingerPanningSettingsProvider(
+            Context context,
+            boolean featureFlagEnabled
+    ) {
+        var defaultValue = isOneFingerPanningEnabledDefault(context);
+        if (featureFlagEnabled) {
+            mContentResolver = context.getContentResolver();
+            mObserver = new ContentObserver(context.getMainThreadHandler()) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mCached.set(isOneFingerPanningEnabledInSetting(context, defaultValue));
+                }
+            };
+            mCached.set(isOneFingerPanningEnabledInSetting(context, defaultValue));
+            mContentResolver.registerContentObserver(URI, false, mObserver);
+        } else {
+            mCached.set(defaultValue);
+        }
+    }
+
+    /** Returns whether one finger panning is enabled.. */
+    public boolean isOneFingerPanningEnabled() {
+        return mCached.get();
+    }
+
+    /** Unregister content observer for listening to secure settings. */
+    public void unregister() {
+        if (mContentResolver != null) {
+            mContentResolver.unregisterContentObserver(mObserver);
+        }
+        mContentResolver = null;
+    }
+
+    private boolean isOneFingerPanningEnabledInSetting(Context context, boolean defaultValue) {
+        return State.ON == Settings.Secure.getIntForUser(
+                mContentResolver,
+                KEY,
+                (defaultValue ? State.ON : State.OFF),
+                context.getUserId());
+    }
+
+    @VisibleForTesting
+    static boolean isOneFingerPanningEnabledDefault(Context context) {
+        boolean oneFingerPanningDefaultValue;
+        try {
+            oneFingerPanningDefaultValue = context.getResources().getBoolean(
+                    com.android.internal.R.bool.config_enable_a11y_magnification_single_panning);
+        } catch (Resources.NotFoundException e) {
+            oneFingerPanningDefaultValue = false;
+        }
+        return oneFingerPanningDefaultValue;
+    }
+}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index b932743..1749ee3 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -84,6 +84,7 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -98,6 +99,7 @@
 import android.service.appwidget.AppWidgetServiceDumpProto;
 import android.service.appwidget.WidgetProto;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.AttributeSet;
@@ -148,6 +150,7 @@
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -159,6 +162,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.LongSupplier;
 
 class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBackupProvider,
         OnCrossProfileWidgetProvidersChangeListener {
@@ -187,6 +191,13 @@
     // used to verify which request has successfully been received by the host.
     private static final AtomicLong UPDATE_COUNTER = new AtomicLong();
 
+    // Default reset interval for generated preview API rate limiting.
+    private static final long DEFAULT_GENERATED_PREVIEW_RESET_INTERVAL_MS =
+            Duration.ofHours(1).toMillis();
+    // Default max API calls per reset interval for generated preview API rate limiting.
+    private static final int DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL = 2;
+
+
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -266,6 +277,8 @@
     // Mark widget lifecycle broadcasts as 'interactive'
     private Bundle mInteractiveBroadcast;
 
+    private ApiCounter mGeneratedPreviewsApiCounter;
+
     AppWidgetServiceImpl(Context context) {
         mContext = context;
     }
@@ -294,6 +307,17 @@
         mIsCombinedBroadcastEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
             SystemUiDeviceConfigFlags.COMBINED_BROADCAST_ENABLED, true);
 
+        final long generatedPreviewResetInterval = DeviceConfig.getLong(NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS,
+                DEFAULT_GENERATED_PREVIEW_RESET_INTERVAL_MS);
+        final int generatedPreviewMaxCallsPerInterval = DeviceConfig.getInt(NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS,
+                DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL);
+        mGeneratedPreviewsApiCounter = new ApiCounter(generatedPreviewResetInterval,
+                generatedPreviewMaxCallsPerInterval);
+        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI,
+                new HandlerExecutor(mCallbackHandler), this::handleSystemUiDeviceConfigChange);
+
         BroadcastOptions opts = BroadcastOptions.makeBasic();
         opts.setBackgroundActivityStartsAllowed(false);
         opts.setInteractive(true);
@@ -476,8 +500,12 @@
             } else {
                 // If the package is being updated, we'll receive a PACKAGE_ADDED
                 // shortly, otherwise it is removed permanently.
-                final boolean packageRemovedPermanently = (extras == null
-                        || !extras.getBoolean(Intent.EXTRA_REPLACING, false));
+                boolean isReplacing = extras != null && extras.getBoolean(Intent.EXTRA_REPLACING,
+                        false);
+                boolean isArchival = extras != null && extras.getBoolean(Intent.EXTRA_ARCHIVAL,
+                        false);
+                final boolean packageRemovedPermanently =
+                        (extras == null || !isReplacing || (isReplacing && isArchival));
 
                 if (packageRemovedPermanently) {
                     for (String pkgName : pkgList) {
@@ -2422,7 +2450,8 @@
         AppWidgetProviderInfo info = createPartialProviderInfo(providerId, ri, existing);
 
         if (android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.disablePrivateSpaceItemsOnHome()) {
+                && android.multiuser.Flags.disablePrivateSpaceItemsOnHome()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
             // Do not add widget providers for profiles with items restricted on home screen.
             if (info != null && mUserManager
                     .getUserProperties(info.getProfile()).areItemsRestrictedOnHomeScreen()) {
@@ -2476,6 +2505,7 @@
     private void deleteProviderLocked(Provider provider) {
         deleteWidgetsLocked(provider, UserHandle.USER_ALL);
         mProviders.remove(provider);
+        mGeneratedPreviewsApiCounter.remove(provider.id);
 
         // no need to send the DISABLE broadcast, since the receiver is gone anyway
         cancelBroadcastsLocked(provider);
@@ -4000,7 +4030,7 @@
     }
 
     @Override
-    public void setWidgetPreview(@NonNull ComponentName providerComponent,
+    public boolean setWidgetPreview(@NonNull ComponentName providerComponent,
             @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
             @NonNull RemoteViews preview) {
         final int userId = UserHandle.getCallingUserId();
@@ -4022,8 +4052,12 @@
                 throw new IllegalArgumentException(
                         providerComponent + " is not a valid AppWidget provider");
             }
-            provider.setGeneratedPreviewLocked(widgetCategories, preview);
-            scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+            if (mGeneratedPreviewsApiCounter.tryApiCall(providerId)) {
+                provider.setGeneratedPreviewLocked(widgetCategories, preview);
+                scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+                return true;
+            }
+            return false;
         }
     }
 
@@ -4064,6 +4098,26 @@
         }
     }
 
+    private void handleSystemUiDeviceConfigChange(DeviceConfig.Properties properties) {
+        Set<String> changed = properties.getKeyset();
+        synchronized (mLock) {
+            if (changed.contains(
+                    SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS)) {
+                long resetIntervalMs = properties.getLong(
+                        SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_RESET_INTERVAL_MS,
+                        /* defaultValue= */ mGeneratedPreviewsApiCounter.getResetIntervalMs());
+                mGeneratedPreviewsApiCounter.setResetIntervalMs(resetIntervalMs);
+            }
+            if (changed.contains(
+                    SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_CALLS_PER_INTERVAL)) {
+                int maxCallsPerInterval = properties.getInt(
+                        SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_CALLS_PER_INTERVAL,
+                        /* defaultValue= */ mGeneratedPreviewsApiCounter.getMaxCallsPerInterval());
+                mGeneratedPreviewsApiCounter.setMaxCallsPerInterval(maxCallsPerInterval);
+            }
+        }
+    }
+
     private final class CallbackHandler extends Handler {
         public static final int MSG_NOTIFY_UPDATE_APP_WIDGET = 1;
         public static final int MSG_NOTIFY_PROVIDER_CHANGED = 2;
@@ -4537,11 +4591,11 @@
         }
     }
 
-    private static final class ProviderId {
+    static final class ProviderId {
         final int uid;
         final ComponentName componentName;
 
-        private ProviderId(int uid, ComponentName componentName) {
+        ProviderId(int uid, ComponentName componentName) {
             this.uid = uid;
             this.componentName = componentName;
         }
@@ -4784,6 +4838,96 @@
         }
     }
 
+    /**
+     * This class keeps track of API calls and implements rate limiting. One instance of this class
+     * tracks calls from all providers for one API, or a group of APIs that should share the same
+     * rate limit.
+     */
+    static final class ApiCounter {
+
+        private static final class ApiCallRecord {
+            // Number of times the API has been called for this provider.
+            public int apiCallCount = 0;
+            // The last time (from SystemClock.elapsedRealtime) the api call count was reset.
+            public long lastResetTimeMs = 0;
+
+            void reset(long nowMs) {
+                apiCallCount = 0;
+                lastResetTimeMs = nowMs;
+            }
+        }
+
+        private final Map<ProviderId, ApiCallRecord> mCallCount = new ArrayMap<>();
+        // The interval at which the call count is reset.
+        private long mResetIntervalMs;
+        // The max number of API calls per interval.
+        private int mMaxCallsPerInterval;
+        // Returns the current time (monotonic). By default this is SystemClock.elapsedRealtime.
+        private LongSupplier mMonotonicClock;
+
+        ApiCounter(long resetIntervalMs, int maxCallsPerInterval) {
+            this(resetIntervalMs, maxCallsPerInterval, SystemClock::elapsedRealtime);
+        }
+
+        ApiCounter(long resetIntervalMs, int maxCallsPerInterval,
+                LongSupplier monotonicClock) {
+            mResetIntervalMs = resetIntervalMs;
+            mMaxCallsPerInterval = maxCallsPerInterval;
+            mMonotonicClock = monotonicClock;
+        }
+
+        public void setResetIntervalMs(long resetIntervalMs) {
+            mResetIntervalMs = resetIntervalMs;
+        }
+
+        public long getResetIntervalMs() {
+            return mResetIntervalMs;
+        }
+
+        public void setMaxCallsPerInterval(int maxCallsPerInterval) {
+            mMaxCallsPerInterval = maxCallsPerInterval;
+        }
+
+        public int getMaxCallsPerInterval() {
+            return mMaxCallsPerInterval;
+        }
+
+        /**
+         * Returns true if the API call for the provider should be allowed, false if it should be
+         * rate-limited.
+         */
+        public boolean tryApiCall(@NonNull ProviderId provider) {
+            final ApiCallRecord record = getOrCreateRecord(provider);
+            final long now = mMonotonicClock.getAsLong();
+            final long timeSinceLastResetMs = now - record.lastResetTimeMs;
+            // If the last reset was beyond the reset interval, reset now.
+            if (timeSinceLastResetMs > mResetIntervalMs) {
+                record.reset(now);
+            }
+            if (record.apiCallCount < mMaxCallsPerInterval) {
+                record.apiCallCount++;
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Remove the provider's call record from this counter, when the provider is no longer
+         * tracked.
+         */
+        public void remove(@NonNull ProviderId id) {
+            mCallCount.remove(id);
+        }
+
+        @NonNull
+        private ApiCallRecord getOrCreateRecord(@NonNull ProviderId provider) {
+            if (!mCallCount.containsKey(provider)) {
+                mCallCount.put(provider, new ApiCallRecord());
+            }
+            return mCallCount.get(provider);
+        }
+    }
+
     private class LoadedWidgetState {
         final Widget widget;
         final int hostTag;
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 551297b..7afb780 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -3935,6 +3935,24 @@
      */
     @GuardedBy("mLock")
     @Nullable
+    private ViewNode getViewNodeFromContextsLocked(@NonNull AutofillId autofillId) {
+        final int numContexts = mContexts.size();
+        for (int i = numContexts - 1; i >= 0; i--) {
+            final FillContext context = mContexts.get(i);
+            final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(),
+                    autofillId);
+            if (node != null) {
+                return node;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets the latest non-empty value for the given id in the autofill contexts.
+     */
+    @GuardedBy("mLock")
+    @Nullable
     private AutofillValue getValueFromContextsLocked(@NonNull AutofillId autofillId) {
         final int numContexts = mContexts.size();
         for (int i = numContexts - 1; i >= 0; i--) {
@@ -6417,7 +6435,21 @@
                     mClient.onGetCredentialException(id, viewId, exception.getType(),
                             exception.getMessage());
                 } else if (response != null) {
-                    mClient.onGetCredentialResponse(id, viewId, response);
+                    if (viewId.isVirtualInt()) {
+                        ViewNode viewNode = getViewNodeFromContextsLocked(viewId);
+                        if (viewNode != null && viewNode.getCredentialManagerCallback() != null) {
+                            Bundle resultData = new Bundle();
+                            resultData.putParcelable(
+                                    CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+                                    response);
+                            viewNode.getCredentialManagerCallback().send(SUCCESS_CREDMAN_SELECTOR,
+                                        resultData);
+                        } else {
+                            Slog.w(TAG, "View node not found after GetCredentialResponse");
+                        }
+                    } else {
+                        mClient.onGetCredentialResponse(id, viewId, response);
+                    }
                 } else {
                     Slog.w(TAG, "sendCredentialManagerResponseToApp called with null response"
                             + "and exception");
diff --git a/services/companion/java/com/android/server/companion/AssociationStore.java b/services/companion/java/com/android/server/companion/AssociationStore.java
deleted file mode 100644
index 01905bb..0000000
--- a/services/companion/java/com/android/server/companion/AssociationStore.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.companion.AssociationInfo;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * Interface for a store of {@link AssociationInfo}-s.
- */
-public interface AssociationStore {
-
-    @IntDef(prefix = { "CHANGE_TYPE_" }, value = {
-            CHANGE_TYPE_ADDED,
-            CHANGE_TYPE_REMOVED,
-            CHANGE_TYPE_UPDATED_ADDRESS_CHANGED,
-            CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    @interface ChangeType {}
-
-    int CHANGE_TYPE_ADDED = 0;
-    int CHANGE_TYPE_REMOVED = 1;
-    int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2;
-    int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3;
-
-    /**  Listener for any changes to {@link AssociationInfo}-s. */
-    interface OnChangeListener {
-        default void onAssociationChanged(
-                @ChangeType int changeType, AssociationInfo association) {
-            switch (changeType) {
-                case CHANGE_TYPE_ADDED:
-                    onAssociationAdded(association);
-                    break;
-
-                case CHANGE_TYPE_REMOVED:
-                    onAssociationRemoved(association);
-                    break;
-
-                case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED:
-                    onAssociationUpdated(association, true);
-                    break;
-
-                case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED:
-                    onAssociationUpdated(association, false);
-                    break;
-            }
-        }
-
-        default void onAssociationAdded(AssociationInfo association) {}
-
-        default void onAssociationRemoved(AssociationInfo association) {}
-
-        default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {}
-    }
-
-    /**
-     * @return all CDM associations.
-     */
-    @NonNull
-    Collection<AssociationInfo> getAssociations();
-
-    /**
-     * @return a {@link List} of associations that belong to the user.
-     */
-    @NonNull
-    List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId);
-
-    /**
-     * @return a {@link List} of association that belong to the package.
-     */
-    @NonNull
-    List<AssociationInfo> getAssociationsForPackage(
-            @UserIdInt int userId, @NonNull String packageName);
-
-    /**
-     * @return an association with the given address that belong to the given package if such an
-     * association exists, otherwise {@code null}.
-     */
-    @Nullable
-    AssociationInfo getAssociationsForPackageWithAddress(
-            @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress);
-
-    /**
-     * @return an association with the given id if such an association exists, otherwise
-     * {@code null}.
-     */
-    @Nullable
-    AssociationInfo getAssociationById(int id);
-
-    /**
-     * @return all associations with the given MAc address.
-     */
-    @NonNull
-    List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress);
-
-    /** Register a {@link OnChangeListener} */
-    void registerListener(@NonNull OnChangeListener listener);
-
-    /** Un-register a previously registered {@link OnChangeListener} */
-    void unregisterListener(@NonNull OnChangeListener listener);
-
-    /** @hide */
-    static String changeTypeToString(@ChangeType int changeType) {
-        switch (changeType) {
-            case CHANGE_TYPE_ADDED:
-                return "ASSOCIATION_ADDED";
-
-            case CHANGE_TYPE_REMOVED:
-                return "ASSOCIATION_REMOVED";
-
-            case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED:
-                return "ASSOCIATION_UPDATED";
-
-            case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED:
-                return "ASSOCIATION_UPDATED_ADDRESS_UNCHANGED";
-
-            default:
-                return "Unknown (" + changeType + ")";
-        }
-    }
-}
diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
index e4cc1f8..f2409fb 100644
--- a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
+++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
@@ -34,6 +34,9 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.CollectionUtils;
+import com.android.server.companion.association.AssociationDiskStore;
+import com.android.server.companion.association.AssociationRequestsProcessor;
+import com.android.server.companion.association.AssociationStore;
 import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
 
 import java.nio.ByteBuffer;
@@ -54,9 +57,9 @@
     @NonNull
     private final PackageManagerInternal mPackageManager;
     @NonNull
-    private final AssociationStoreImpl mAssociationStore;
+    private final AssociationStore mAssociationStore;
     @NonNull
-    private final PersistentDataStore mPersistentStore;
+    private final AssociationDiskStore mPersistentStore;
     @NonNull
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     @NonNull
@@ -71,8 +74,8 @@
             new PerUserAssociationSet();
 
     BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service,
-                           @NonNull AssociationStoreImpl associationStore,
-                           @NonNull PersistentDataStore persistentStore,
+                           @NonNull AssociationStore associationStore,
+                           @NonNull AssociationDiskStore persistentStore,
                            @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore,
                            @NonNull AssociationRequestsProcessor associationRequestsProcessor) {
         mService = service;
diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
index 559ebbc..c801489 100644
--- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java
+++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.infra.PerUser;
+import com.android.server.companion.association.AssociationStore;
 import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
 import com.android.server.companion.presence.ObservableUuid;
 import com.android.server.companion.presence.ObservableUuidStore;
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 17ba073..e4a1048 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -37,7 +37,9 @@
 import static com.android.internal.util.CollectionUtils.any;
 import static com.android.internal.util.Preconditions.checkState;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.server.companion.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
+import static com.android.server.companion.association.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
+import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser;
+import static com.android.server.companion.utils.AssociationUtils.getLastAssociationIdForUser;
 import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
 import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
 import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
@@ -117,6 +119,11 @@
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.companion.association.AssociationDiskStore;
+import com.android.server.companion.association.AssociationRequestsProcessor;
+import com.android.server.companion.association.AssociationRevokeProcessor;
+import com.android.server.companion.association.AssociationStore;
+import com.android.server.companion.association.InactiveAssociationsRemovalService;
 import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
 import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
 import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall;
@@ -147,8 +154,6 @@
     static final String TAG = "CDM_CompanionDeviceManagerService";
     static final boolean DEBUG = false;
 
-    /** Range of Association IDs allocated for a user. */
-    private static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000;
     private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
 
     private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
@@ -160,10 +165,10 @@
     private static final int MAX_CN_LENGTH = 500;
 
     private final ActivityManager mActivityManager;
-    private PersistentDataStore mPersistentStore;
+    private AssociationDiskStore mAssociationDiskStore;
     private final PersistUserStateHandler mUserPersistenceHandler;
 
-    private final AssociationStoreImpl mAssociationStore;
+    private final AssociationStore mAssociationStore;
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     private AssociationRequestsProcessor mAssociationRequestsProcessor;
     private SystemDataTransferProcessor mSystemDataTransferProcessor;
@@ -178,7 +183,7 @@
     private final IAppOpsService mAppOpsManager;
     private final PowerWhitelistManager mPowerWhitelistManager;
     private final UserManager mUserManager;
-    final PackageManagerInternal mPackageManagerInternal;
+    public final PackageManagerInternal mPackageManagerInternal;
     private final PowerManagerInternal mPowerManagerInternal;
 
     /**
@@ -210,7 +215,7 @@
         mUserManager = context.getSystemService(UserManager.class);
 
         mUserPersistenceHandler = new PersistUserStateHandler();
-        mAssociationStore = new AssociationStoreImpl();
+        mAssociationStore = new AssociationStore();
         mSystemDataTransferRequestStore = new SystemDataTransferRequestStore();
 
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
@@ -221,11 +226,11 @@
     public void onStart() {
         final Context context = getContext();
 
-        mPersistentStore = new PersistentDataStore();
+        mAssociationDiskStore = new AssociationDiskStore();
         mAssociationRequestsProcessor = new AssociationRequestsProcessor(
                 /* cdmService */ this, mAssociationStore);
         mBackupRestoreProcessor = new BackupRestoreProcessor(
-                /* cdmService */ this, mAssociationStore, mPersistentStore,
+                /* cdmService */ this, mAssociationStore, mAssociationDiskStore,
                 mSystemDataTransferRequestStore, mAssociationRequestsProcessor);
 
         mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
@@ -264,10 +269,13 @@
     void loadAssociationsFromDisk() {
         final Set<AssociationInfo> allAssociations = new ArraySet<>();
         synchronized (mPreviouslyUsedIds) {
+            List<Integer> userIds = new ArrayList<>();
+            for (UserInfo user : mUserManager.getAliveUsers()) {
+                userIds.add(user.id);
+            }
             // The data is stored in DE directories, so we can read the data for all users now
             // (which would not be possible if the data was stored to CE directories).
-            mPersistentStore.readStateForUsers(
-                    mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds);
+            mAssociationDiskStore.readStateForUsers(userIds, allAssociations, mPreviouslyUsedIds);
         }
 
         final Set<AssociationInfo> activeAssociations =
@@ -291,7 +299,7 @@
             }
         }
 
-        mAssociationStore.setAssociations(activeAssociations);
+        mAssociationStore.setAssociationsToCache(activeAssociations);
 
         // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because
         // persistStateForUser() queries AssociationStore.
@@ -582,7 +590,7 @@
 
         final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId);
 
-        mPersistentStore.persistStateForUser(userId, allAssociations, usedIdsForUser);
+        mAssociationDiskStore.persistStateForUser(userId, allAssociations, usedIdsForUser);
     }
 
     private void notifyListeners(
@@ -646,7 +654,8 @@
         final List<AssociationInfo> associationsForPackage =
                 mAssociationStore.getAssociationsForPackage(userId, packageName);
         for (AssociationInfo association : associationsForPackage) {
-            updateSpecialAccessPermissionForAssociatedPackage(association);
+            updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
+                    association.getPackageName());
         }
 
         mCompanionAppController.onPackagesChanged(userId);
@@ -692,7 +701,7 @@
         }
     }
 
-    class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
+    public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
         @Override
         public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
                 throws RemoteException {
@@ -1338,7 +1347,10 @@
         return usedIdsForPackage;
     }
 
-    int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) {
+    /**
+     * Get a new association id for the package.
+     */
+    public int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) {
         synchronized (mPreviouslyUsedIds) {
             // First: collect all IDs currently in use for this user's Associations.
             final SparseBooleanArray usedIds = new SparseBooleanArray();
@@ -1383,9 +1395,12 @@
         }
     }
 
-    void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
+    /**
+     * Update special access for the association's package
+     */
+    public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) {
         final PackageInfo packageInfo =
-                getPackageInfo(getContext(), association.getUserId(), association.getPackageName());
+                getPackageInfo(getContext(), userId, packageName);
 
         Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo));
     }
@@ -1539,15 +1554,6 @@
         }
     };
 
-    static int getFirstAssociationIdForUser(@UserIdInt int userId) {
-        // We want the IDs to start from 1, not 0.
-        return userId * ASSOCIATIONS_IDS_PER_USER_RANGE + 1;
-    }
-
-    static int getLastAssociationIdForUser(@UserIdInt int userId) {
-        return (userId + 1) * ASSOCIATIONS_IDS_PER_USER_RANGE;
-    }
-
     private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) {
         final Map<String, Set<Integer>> copy = new HashMap<>();
 
@@ -1671,11 +1677,17 @@
         }
     }
 
-    void postPersistUserState(@UserIdInt int userId) {
+    /**
+     * Persist associations
+     */
+    public void postPersistUserState(@UserIdInt int userId) {
         mUserPersistenceHandler.postPersistUserState(userId);
     }
 
-    static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
+    /**
+     * Set to store associations
+     */
+    public static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
         @Override
         protected @NonNull Set<AssociationInfo> create(int userId) {
             return new ArraySet<>();
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 74b4cab..16877dc 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -32,6 +32,9 @@
 import android.util.Base64;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.server.companion.association.AssociationRequestsProcessor;
+import com.android.server.companion.association.AssociationRevokeProcessor;
+import com.android.server.companion.association.AssociationStore;
 import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
 import com.android.server.companion.datatransfer.contextsync.BitmapUtils;
 import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController;
@@ -47,7 +50,7 @@
 
     private final CompanionDeviceManagerService mService;
     private final AssociationRevokeProcessor mRevokeProcessor;
-    private final AssociationStoreImpl mAssociationStore;
+    private final AssociationStore mAssociationStore;
     private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private final CompanionTransportManager mTransportManager;
 
@@ -56,7 +59,7 @@
     private final BackupRestoreProcessor mBackupRestoreProcessor;
 
     CompanionDeviceShellCommand(CompanionDeviceManagerService service,
-            AssociationStoreImpl associationStore,
+            AssociationStore associationStore,
             CompanionDevicePresenceMonitor devicePresenceMonitor,
             CompanionTransportManager transportManager,
             SystemDataTransferProcessor systemDataTransferProcessor,
diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java
similarity index 92%
rename from services/companion/java/com/android/server/companion/PersistentDataStore.java
rename to services/companion/java/com/android/server/companion/association/AssociationDiskStore.java
index 7527efb..75cb120 100644
--- a/services/companion/java/com/android/server/companion/PersistentDataStore.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.companion;
+package com.android.server.companion.association;
 
 import static com.android.internal.util.CollectionUtils.forEach;
 import static com.android.internal.util.XmlUtils.readBooleanAttribute;
@@ -25,8 +25,8 @@
 import static com.android.internal.util.XmlUtils.writeIntAttribute;
 import static com.android.internal.util.XmlUtils.writeLongAttribute;
 import static com.android.internal.util.XmlUtils.writeStringAttribute;
-import static com.android.server.companion.CompanionDeviceManagerService.getFirstAssociationIdForUser;
-import static com.android.server.companion.CompanionDeviceManagerService.getLastAssociationIdForUser;
+import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser;
+import static com.android.server.companion.utils.AssociationUtils.getLastAssociationIdForUser;
 import static com.android.server.companion.utils.DataStoreUtils.createStorageFileForUser;
 import static com.android.server.companion.utils.DataStoreUtils.fileToByteArray;
 import static com.android.server.companion.utils.DataStoreUtils.isEndOfTag;
@@ -38,12 +38,10 @@
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.companion.AssociationInfo;
-import android.content.pm.UserInfo;
 import android.net.MacAddress;
 import android.os.Environment;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
-import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.Xml;
@@ -51,7 +49,6 @@
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
-import com.android.server.companion.utils.DataStoreUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -71,6 +68,8 @@
 import java.util.concurrent.ConcurrentMap;
 
 /**
+ * IMPORTANT: This class should NOT be directly used except {@link AssociationStore}
+ *
  * The class responsible for persisting Association records and other related information (such as
  * previously used IDs) to a disk, and reading the data back from the disk.
  *
@@ -107,8 +106,6 @@
  * Since Android T the data is stored to "companion_device_manager.xml" file in
  * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}.
  *
- * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)}
- *
  * <p>
  * Since Android T the data is stored using the v1 schema.
  *
@@ -161,9 +158,8 @@
  * }</pre>
  */
 @SuppressLint("LongLogTag")
-final class PersistentDataStore {
-    private static final String TAG = "CompanionDevice_PersistentDataStore";
-    private static final boolean DEBUG = CompanionDeviceManagerService.DEBUG;
+public final class AssociationDiskStore {
+    private static final String TAG = "CompanionDevice_AssociationDiskStore";
 
     private static final int CURRENT_PERSISTENCE_VERSION = 1;
 
@@ -200,11 +196,13 @@
     private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile =
             new ConcurrentHashMap<>();
 
-    void readStateForUsers(@NonNull List<UserInfo> users,
+    /**
+     * Read all associations for given users
+     */
+    public void readStateForUsers(@NonNull List<Integer> userIds,
             @NonNull Set<AssociationInfo> allAssociationsOut,
             @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut) {
-        for (UserInfo user : users) {
-            final int userId = user.id;
+        for (int userId : userIds) {
             // Previously used IDs are stored in the "out" collection per-user.
             final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>();
 
@@ -247,12 +245,11 @@
      * @param associationsOut a container to read the {@link AssociationInfo}s "into".
      * @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into".
      */
-    void readStateForUser(@UserIdInt int userId,
+    private void readStateForUser(@UserIdInt int userId,
             @NonNull Collection<AssociationInfo> associationsOut,
             @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) {
         Slog.i(TAG, "Reading associations for user " + userId + " from disk");
         final AtomicFile file = getStorageFileForUser(userId);
-        if (DEBUG) Log.d(TAG, "  > File=" + file.getBaseFile().getPath());
 
         // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
         // accesses to the file on the file system using this AtomicFile object.
@@ -261,12 +258,8 @@
             final AtomicFile readFrom;
             final String rootTag;
             if (!file.getBaseFile().exists()) {
-                if (DEBUG) Log.d(TAG, "  > File does not exist -> Try to read legacy file");
-
                 legacyBaseFile = getBaseLegacyStorageFileForUser(userId);
-                if (DEBUG) Log.d(TAG, "  > Legacy file=" + legacyBaseFile.getPath());
                 if (!legacyBaseFile.exists()) {
-                    if (DEBUG) Log.d(TAG, "  > Legacy file does not exist -> Abort");
                     return;
                 }
 
@@ -277,27 +270,16 @@
                 rootTag = XML_TAG_STATE;
             }
 
-            if (DEBUG) Log.d(TAG, "  > Reading associations...");
             final int version = readStateFromFileLocked(userId, readFrom, rootTag,
                     associationsOut, previouslyUsedIdsPerPackageOut);
-            if (DEBUG) {
-                Log.d(TAG, "  > Done reading: " + associationsOut);
-                if (version < CURRENT_PERSISTENCE_VERSION) {
-                    Log.d(TAG, "  > File used old format: v." + version + " -> Re-write");
-                }
-            }
 
             if (legacyBaseFile != null || version < CURRENT_PERSISTENCE_VERSION) {
                 // The data is either in the legacy file or in the legacy format, or both.
                 // Save the data to right file in using the current format.
-                if (DEBUG) {
-                    Log.d(TAG, "  > Writing the data to " + file.getBaseFile().getPath());
-                }
                 persistStateToFileLocked(file, associationsOut, previouslyUsedIdsPerPackageOut);
 
                 if (legacyBaseFile != null) {
                     // We saved the data to the right file, can delete the old file now.
-                    if (DEBUG) Log.d(TAG, "  > Deleting legacy file");
                     legacyBaseFile.delete();
                 }
             }
@@ -314,14 +296,12 @@
      * @param associations a set of user's associations.
      * @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user.
      */
-    void persistStateForUser(@UserIdInt int userId,
+    public void persistStateForUser(@UserIdInt int userId,
             @NonNull Collection<AssociationInfo> associations,
             @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) {
         Slog.i(TAG, "Writing associations for user " + userId + " to disk");
-        if (DEBUG) Slog.d(TAG, "  > " + associations);
 
         final AtomicFile file = getStorageFileForUser(userId);
-        if (DEBUG) Log.d(TAG, "  > File=" + file.getBaseFile().getPath());
         // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
         // accesses to the file on the file system using this AtomicFile object.
         synchronized (file) {
@@ -404,7 +384,10 @@
                 u -> createStorageFileForUser(userId, FILE_NAME));
     }
 
-    byte[] getBackupPayload(@UserIdInt int userId) {
+    /**
+     * Get associations backup payload from disk
+     */
+    public byte[] getBackupPayload(@UserIdInt int userId) {
         Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk");
         final AtomicFile file = getStorageFileForUser(userId);
 
@@ -413,7 +396,10 @@
         }
     }
 
-    void readStateFromPayload(byte[] payload, @UserIdInt int userId,
+    /**
+     * Convert payload to a set of associations
+     */
+    public void readStateFromPayload(byte[] payload, @UserIdInt int userId,
                               @NonNull Set<AssociationInfo> associationsOut,
                               @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) {
         try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) {
@@ -615,7 +601,7 @@
                     macAddress, displayName, profile, null, selfManaged, notify,
                     revoked, pending, timeApproved, lastTimeConnected, systemDataSyncFlags);
         } catch (Exception e) {
-            if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e);
+            Slog.e(TAG, "Could not create AssociationInfo", e);
         }
         return associationInfo;
     }
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
similarity index 89%
rename from services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
rename to services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index 1dab40e..29ec7c2 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.companion;
+package com.android.server.companion.association;
 
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -24,7 +24,6 @@
 import static android.content.ComponentName.createRelative;
 import static android.content.pm.PackageManager.FEATURE_WATCH;
 
-import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
 import static com.android.server.companion.utils.MetricUtils.logCreateAssociation;
 import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
 import static com.android.server.companion.utils.PermissionsUtils.enforcePermissionForCreatingAssociation;
@@ -59,6 +58,7 @@
 import android.util.Slog;
 
 import com.android.internal.R;
+import com.android.server.companion.CompanionDeviceManagerService;
 import com.android.server.companion.utils.PackageUtils;
 
 import java.util.List;
@@ -107,7 +107,7 @@
  * ResultReceiver, MacAddress)
  */
 @SuppressLint("LongLogTag")
-class AssociationRequestsProcessor {
+public class AssociationRequestsProcessor {
     private static final String TAG = "CDM_AssociationRequestsProcessor";
 
     // AssociationRequestsProcessor <-> UI
@@ -130,12 +130,12 @@
     private final @NonNull Context mContext;
     private final @NonNull CompanionDeviceManagerService mService;
     private final @NonNull PackageManagerInternal mPackageManager;
-    private final @NonNull AssociationStoreImpl mAssociationStore;
+    private final @NonNull AssociationStore mAssociationStore;
     @NonNull
     private final ComponentName mCompanionDeviceActivity;
 
-    AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
-            @NonNull AssociationStoreImpl associationStore) {
+    public AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
+            @NonNull AssociationStore associationStore) {
         mContext = service.getContext();
         mService = service;
         mPackageManager = service.mPackageManagerInternal;
@@ -149,7 +149,7 @@
      * Handle incoming {@link AssociationRequest}s, sent via
      * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
      */
-    void processNewAssociationRequest(@NonNull AssociationRequest request,
+    public void processNewAssociationRequest(@NonNull AssociationRequest request,
             @NonNull String packageName, @UserIdInt int userId,
             @NonNull IAssociationRequestCallback callback) {
         requireNonNull(request, "Request MUST NOT be null");
@@ -161,11 +161,8 @@
         requireNonNull(callback, "Callback MUST NOT be null");
 
         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
-        if (DEBUG) {
-            Slog.d(TAG, "processNewAssociationRequest() "
-                    + "request=" + request + ", "
-                    + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")");
-        }
+        Slog.d(TAG, "processNewAssociationRequest() " + "request=" + request + ", " + "package=u"
+                + userId + "/" + packageName + " (uid=" + packageUid + ")");
 
         // 1. Enforce permissions and other requirements.
         enforcePermissionForCreatingAssociation(mContext, request, packageUid);
@@ -223,7 +220,7 @@
     /**
      * Process another AssociationRequest in CompanionDeviceActivity to cancel current dialog.
      */
-    PendingIntent buildAssociationCancellationIntent(@NonNull String packageName,
+    public PendingIntent buildAssociationCancellationIntent(@NonNull String packageName,
             @UserIdInt int userId) {
         requireNonNull(packageName, "Package name MUST NOT be null");
 
@@ -248,13 +245,6 @@
         final int userId = request.getUserId();
         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
 
-        if (DEBUG) {
-            Slog.d(TAG, "processAssociationRequestApproval()\n"
-                    + "   package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n"
-                    + "   request=" + request + "\n"
-                    + "   macAddress=" + macAddress + "\n");
-        }
-
         // 1. Need to check permissions again in case something changed, since we first received
         // this request.
         try {
@@ -288,6 +278,9 @@
         }
     }
 
+    /**
+     * Create an association.
+     */
     public void createAssociation(@UserIdInt int userId, @NonNull String packageName,
             @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
             @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice,
@@ -309,6 +302,9 @@
         // that there are other devices with the same profile, so the role holder won't be removed.
     }
 
+    /**
+     * Grant a role if specified and add an association to store.
+     */
     public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association,
             @Nullable IAssociationRequestCallback callback,
             @Nullable ResultReceiver resultReceiver) {
@@ -331,6 +327,9 @@
         });
     }
 
+    /**
+     * Enable system data sync.
+     */
     public void enableSystemDataSync(int associationId, int flags) {
         AssociationInfo association = mAssociationStore.getAssociationById(associationId);
         AssociationInfo updated = (new AssociationInfo.Builder(association))
@@ -338,6 +337,9 @@
         mAssociationStore.updateAssociation(updated);
     }
 
+    /**
+     * Disable system data sync.
+     */
     public void disableSystemDataSync(int associationId, int flags) {
         AssociationInfo association = mAssociationStore.getAssociationById(associationId);
         AssociationInfo updated = (new AssociationInfo.Builder(association))
@@ -350,7 +352,8 @@
 
         mAssociationStore.addAssociation(association);
 
-        mService.updateSpecialAccessPermissionForAssociatedPackage(association);
+        mService.updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
+                association.getPackageName());
 
         logCreateAssociation(association.getDeviceProfile());
     }
@@ -431,38 +434,37 @@
 
     private final ResultReceiver mOnRequestConfirmationReceiver =
             new ResultReceiver(Handler.getMain()) {
-        @Override
-        protected void onReceiveResult(int resultCode, Bundle data) {
-            if (DEBUG) {
-                Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() "
-                        + "code=" + resultCode + ", " + "data=" + data);
-            }
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle data) {
+                    if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
+                        Slog.w(TAG, "Unknown result code:" + resultCode);
+                        return;
+                    }
 
-            if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
-                Slog.w(TAG, "Unknown result code:" + resultCode);
-                return;
-            }
+                    final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST,
+                            android.companion.AssociationRequest.class);
+                    final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
+                            .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
+                    final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER,
+                            android.os.ResultReceiver.class);
 
-            final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST, android.companion.AssociationRequest.class);
-            final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
-                    .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
-            final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER, android.os.ResultReceiver.class);
+                    requireNonNull(request);
+                    requireNonNull(callback);
+                    requireNonNull(resultReceiver);
 
-            requireNonNull(request);
-            requireNonNull(callback);
-            requireNonNull(resultReceiver);
+                    final MacAddress macAddress;
+                    if (request.isSelfManaged()) {
+                        macAddress = null;
+                    } else {
+                        macAddress = data.getParcelable(EXTRA_MAC_ADDRESS,
+                                android.net.MacAddress.class);
+                        requireNonNull(macAddress);
+                    }
 
-            final MacAddress macAddress;
-            if (request.isSelfManaged()) {
-                macAddress = null;
-            } else {
-                macAddress = data.getParcelable(EXTRA_MAC_ADDRESS, android.net.MacAddress.class);
-                requireNonNull(macAddress);
-            }
-
-            processAssociationRequestApproval(request, callback, resultReceiver, macAddress);
-        }
-    };
+                    processAssociationRequestApproval(request, callback, resultReceiver,
+                            macAddress);
+                }
+            };
 
     private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
         // Throttle frequent associations
diff --git a/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java
similarity index 95%
rename from services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java
rename to services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java
index 10963ea..490be0d 100644
--- a/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.companion;
+package com.android.server.companion.association;
 
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
@@ -38,6 +38,8 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.companion.CompanionApplicationController;
+import com.android.server.companion.CompanionDeviceManagerService;
 import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
 import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
 
@@ -55,7 +57,7 @@
     private static final boolean DEBUG = false;
     private final @NonNull Context mContext;
     private final @NonNull CompanionDeviceManagerService mService;
-    private final @NonNull AssociationStoreImpl mAssociationStore;
+    private final @NonNull AssociationStore mAssociationStore;
     private final @NonNull PackageManagerInternal mPackageManagerInternal;
     private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private final @NonNull SystemDataTransferRequestStore mSystemDataTransferRequestStore;
@@ -90,8 +92,8 @@
     @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
     private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>();
 
-    AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service,
-            @NonNull AssociationStoreImpl associationStore,
+    public AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service,
+            @NonNull AssociationStore associationStore,
             @NonNull PackageManagerInternal packageManager,
             @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor,
             @NonNull CompanionApplicationController applicationController,
@@ -108,8 +110,11 @@
         mSystemDataTransferRequestStore = systemDataTransferRequestStore;
     }
 
+    /**
+     * Disassociate an association
+     */
     // TODO: also revoke notification access
-    void disassociateInternal(int associationId) {
+    public void disassociateInternal(int associationId) {
         final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
         final int userId = association.getUserId();
         final String packageName = association.getPackageName();
@@ -168,7 +173,7 @@
      *         {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process,
      *         which would lead to the poor UX, hence need to try later.
      */
-    boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) {
+    public boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) {
         if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association);
         final String deviceProfile = association.getDeviceProfile();
 
@@ -208,15 +213,6 @@
         return true;
     }
 
-    @SuppressLint("MissingPermission")
-    private int  getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
-        return Binder.withCleanCallingIdentity(() -> {
-            final int uid =
-                    mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
-            return mActivityManager.getUidImportance(uid);
-        });
-    }
-
     /**
      * Set revoked flag for active association and add the revoked association and the uid into
      * the caches.
@@ -225,7 +221,7 @@
      * @see #mUidsPendingRoleHolderRemoval
      * @see OnPackageVisibilityChangeListener
      */
-    void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
+    public void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
         // First: set revoked flag
         association = (new AssociationInfo.Builder(association)).setRevoked(true).build();
         final String packageName = association.getPackageName();
@@ -247,6 +243,28 @@
     }
 
     /**
+     * @return a copy of the revoked associations set (safeguarding against
+     *         {@code ConcurrentModificationException}-s).
+     */
+    @NonNull
+    public Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
+            @UserIdInt int userId) {
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            // Return a copy.
+            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    private int  getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
+        return Binder.withCleanCallingIdentity(() -> {
+            final int uid =
+                    mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+            return mActivityManager.getUidImportance(uid);
+        });
+    }
+
+    /**
      * Remove the revoked association from the cache and also remove the uid from the map if
      * there are other associations with the same package still pending for role holder removal.
      *
@@ -279,18 +297,6 @@
         }
     }
 
-    /**
-     * @return a copy of the revoked associations set (safeguarding against
-     *         {@code ConcurrentModificationException}-s).
-     */
-    @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
-            @UserIdInt int userId) {
-        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
-            // Return a copy.
-            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
-        }
-    }
-
     private String getPackageNameByUid(int uid) {
         synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
             return mUidsPendingRoleHolderRemoval.get(uid);
diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java
similarity index 68%
rename from services/companion/java/com/android/server/companion/AssociationStoreImpl.java
rename to services/companion/java/com/android/server/companion/association/AssociationStore.java
index 8c6ad3b..2f94bde 100644
--- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.server.companion;
+package com.android.server.companion.association;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.companion.AssociationInfo;
 import android.net.MacAddress;
-import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
 
@@ -30,6 +30,8 @@
 import com.android.internal.util.CollectionUtils;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -40,24 +42,69 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.StringJoiner;
 
 /**
- * Implementation of the {@link AssociationStore}, with addition of the methods for modification.
- * <ul>
- * <li> {@link #addAssociation(AssociationInfo)}
- * <li> {@link #removeAssociation(int)}
- * <li> {@link #updateAssociation(AssociationInfo)}
- * </ul>
- *
- * The class has package-private access level, and instances of the class should only be created by
- * the {@link CompanionDeviceManagerService}.
- * Other system component (both inside and outside if the com.android.server.companion package)
- * should use public {@link AssociationStore} interface.
+ * Association store for CRUD.
  */
 @SuppressLint("LongLogTag")
-class AssociationStoreImpl implements AssociationStore {
-    private static final boolean DEBUG = false;
+public class AssociationStore {
+
+    @IntDef(prefix = { "CHANGE_TYPE_" }, value = {
+            CHANGE_TYPE_ADDED,
+            CHANGE_TYPE_REMOVED,
+            CHANGE_TYPE_UPDATED_ADDRESS_CHANGED,
+            CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ChangeType {}
+
+    public static final int CHANGE_TYPE_ADDED = 0;
+    public static final int CHANGE_TYPE_REMOVED = 1;
+    public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2;
+    public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3;
+
+    /**  Listener for any changes to associations. */
+    public interface OnChangeListener {
+        /**
+         * Called when there are association changes.
+         */
+        default void onAssociationChanged(
+                @AssociationStore.ChangeType int changeType, AssociationInfo association) {
+            switch (changeType) {
+                case CHANGE_TYPE_ADDED:
+                    onAssociationAdded(association);
+                    break;
+
+                case CHANGE_TYPE_REMOVED:
+                    onAssociationRemoved(association);
+                    break;
+
+                case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED:
+                    onAssociationUpdated(association, true);
+                    break;
+
+                case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED:
+                    onAssociationUpdated(association, false);
+                    break;
+            }
+        }
+
+        /**
+         * Called when an association is added.
+         */
+        default void onAssociationAdded(AssociationInfo association) {}
+
+        /**
+         * Called when an association is removed.
+         */
+        default void onAssociationRemoved(AssociationInfo association) {}
+
+        /**
+         * Called when an association is updated.
+         */
+        default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {}
+    }
+
     private static final String TAG = "CDM_AssociationStore";
 
     private final Object mLock = new Object();
@@ -72,17 +119,17 @@
     @GuardedBy("mListeners")
     private final Set<OnChangeListener> mListeners = new LinkedHashSet<>();
 
-    void addAssociation(@NonNull AssociationInfo association) {
+    /**
+     * Add an association.
+     */
+    public void addAssociation(@NonNull AssociationInfo association) {
+        Slog.i(TAG, "Adding new association=" + association);
+
         // Validity check first.
         checkNotRevoked(association);
 
         final int id = association.getId();
 
-        if (DEBUG) {
-            Log.i(TAG, "addAssociation() " + association.toShortString());
-            Log.d(TAG, "  association=" + association);
-        }
-
         synchronized (mLock) {
             if (mIdMap.containsKey(id)) {
                 Slog.e(TAG, "Association with id " + id + " already exists.");
@@ -96,34 +143,34 @@
             }
 
             invalidateCacheForUserLocked(association.getUserId());
+
+            Slog.i(TAG, "Done adding new association.");
         }
 
         broadcastChange(CHANGE_TYPE_ADDED, association);
     }
 
-    void updateAssociation(@NonNull AssociationInfo updated) {
+    /**
+     * Update an association.
+     */
+    public void updateAssociation(@NonNull AssociationInfo updated) {
+        Slog.i(TAG, "Updating new association=" + updated);
         // Validity check first.
         checkNotRevoked(updated);
 
         final int id = updated.getId();
 
-        if (DEBUG) {
-            Log.i(TAG, "updateAssociation() " + updated.toShortString());
-            Log.d(TAG, "  updated=" + updated);
-        }
-
         final AssociationInfo current;
         final boolean macAddressChanged;
         synchronized (mLock) {
             current = mIdMap.get(id);
             if (current == null) {
-                if (DEBUG) Log.w(TAG, "Association with id " + id + " does not exist.");
+                Slog.w(TAG, "Can't update association. It does not exist.");
                 return;
             }
-            if (DEBUG) Log.d(TAG, "  current=" + current);
 
             if (current.equals(updated)) {
-                if (DEBUG) Log.w(TAG, "  No changes.");
+                Slog.w(TAG, "Association is the same.");
                 return;
             }
 
@@ -144,6 +191,7 @@
                     mAddressMap.computeIfAbsent(updatedAddress, it -> new HashSet<>()).add(id);
                 }
             }
+            Slog.i(TAG, "Done updating association.");
         }
 
         final int changeType = macAddressChanged ? CHANGE_TYPE_UPDATED_ADDRESS_CHANGED
@@ -151,21 +199,19 @@
         broadcastChange(changeType, updated);
     }
 
-    void removeAssociation(int id) {
-        if (DEBUG) Log.i(TAG, "removeAssociation() id=" + id);
+    /**
+     * Remove an association
+     */
+    public void removeAssociation(int id) {
+        Slog.i(TAG, "Removing association id=" + id);
 
         final AssociationInfo association;
         synchronized (mLock) {
             association = mIdMap.remove(id);
 
             if (association == null) {
-                if (DEBUG) Log.w(TAG, "Association with id " + id + " is not stored.");
+                Slog.w(TAG, "Can't remove association. It does not exist.");
                 return;
-            } else {
-                if (DEBUG) {
-                    Log.i(TAG, "removed " + association.toShortString());
-                    Log.d(TAG, "  association=" + association);
-                }
             }
 
             final MacAddress macAddress = association.getDeviceMacAddress();
@@ -174,6 +220,8 @@
             }
 
             invalidateCacheForUserLocked(association.getUserId());
+
+            Slog.i(TAG, "Done removing association.");
         }
 
         broadcastChange(CHANGE_TYPE_REMOVED, association);
@@ -195,12 +243,18 @@
         }
     }
 
+    /**
+     * Get associations for the user.
+     */
     public @NonNull List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId) {
         synchronized (mLock) {
             return getAssociationsForUserLocked(userId);
         }
     }
 
+    /**
+     * Get associations for the package
+     */
     public @NonNull List<AssociationInfo> getAssociationsForPackage(
             @UserIdInt int userId, @NonNull String packageName) {
         final List<AssociationInfo> associationsForUser = getAssociationsForUser(userId);
@@ -210,6 +264,9 @@
         return Collections.unmodifiableList(associationsForPackage);
     }
 
+    /**
+     * Get associations by mac address for the package.
+     */
     public @Nullable AssociationInfo getAssociationsForPackageWithAddress(
             @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
         final List<AssociationInfo> associations = getAssociationsByAddress(macAddress);
@@ -217,13 +274,20 @@
                 it -> it.belongsToPackage(userId, packageName));
     }
 
+    /**
+     * Get association by id.
+     */
     public @Nullable AssociationInfo getAssociationById(int id) {
         synchronized (mLock) {
             return mIdMap.get(id);
         }
     }
 
-    public @NonNull List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) {
+    /**
+     * Get associations by mac address.
+     */
+    @NonNull
+    public List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) {
         final MacAddress address = MacAddress.fromString(macAddress);
 
         synchronized (mLock) {
@@ -240,7 +304,8 @@
     }
 
     @GuardedBy("mLock")
-    private @NonNull List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) {
+    @NonNull
+    private List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) {
         final List<AssociationInfo> cached = mCachedPerUser.get(userId);
         if (cached != null) {
             return cached;
@@ -262,12 +327,18 @@
         mCachedPerUser.delete(userId);
     }
 
+    /**
+     * Register a listener for association changes.
+     */
     public void registerListener(@NonNull OnChangeListener listener) {
         synchronized (mListeners) {
             mListeners.add(listener);
         }
     }
 
+    /**
+     * Unregister a listener previously registered for association changes.
+     */
     public void unregisterListener(@NonNull OnChangeListener listener) {
         synchronized (mListeners) {
             mListeners.remove(listener);
@@ -297,43 +368,30 @@
         }
     }
 
-    void setAssociations(Collection<AssociationInfo> allAssociations) {
+    /**
+     * Set associations to cache. It will clear the existing cache.
+     */
+    public void setAssociationsToCache(Collection<AssociationInfo> associations) {
         // Validity check first.
-        allAssociations.forEach(AssociationStoreImpl::checkNotRevoked);
+        associations.forEach(AssociationStore::checkNotRevoked);
 
-        if (DEBUG) {
-            Log.i(TAG, "setAssociations() n=" + allAssociations.size());
-            final StringJoiner stringJoiner = new StringJoiner(", ");
-            allAssociations.forEach(assoc -> stringJoiner.add(assoc.toShortString()));
-            Log.v(TAG, "  associations=" + stringJoiner);
-        }
         synchronized (mLock) {
-            setAssociationsLocked(allAssociations);
-        }
-    }
+            mIdMap.clear();
+            mAddressMap.clear();
+            mCachedPerUser.clear();
 
-    @GuardedBy("mLock")
-    private void setAssociationsLocked(Collection<AssociationInfo> associations) {
-        clearLocked();
+            for (AssociationInfo association : associations) {
+                final int id = association.getId();
+                mIdMap.put(id, association);
 
-        for (AssociationInfo association : associations) {
-            final int id = association.getId();
-            mIdMap.put(id, association);
-
-            final MacAddress address = association.getDeviceMacAddress();
-            if (address != null) {
-                mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id);
+                final MacAddress address = association.getDeviceMacAddress();
+                if (address != null) {
+                    mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id);
+                }
             }
         }
     }
 
-    @GuardedBy("mLock")
-    private void clearLocked() {
-        mIdMap.clear();
-        mAddressMap.clear();
-        mCachedPerUser.clear();
-    }
-
     private static void checkNotRevoked(@NonNull AssociationInfo association) {
         if (association.isRevoked()) {
             throw new IllegalArgumentException(
diff --git a/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
similarity index 84%
rename from services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
rename to services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
index aac628c..894c49a 100644
--- a/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
+++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
@@ -14,9 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.companion;
-
-import static com.android.server.companion.CompanionDeviceManagerService.TAG;
+package com.android.server.companion.association;
 
 import static java.util.concurrent.TimeUnit.DAYS;
 
@@ -29,13 +27,17 @@
 import android.util.Slog;
 
 import com.android.server.LocalServices;
+import com.android.server.companion.CompanionDeviceManagerServiceInternal;
 
 /**
- * A Job Service responsible for clean up the Association.
+ * A Job Service responsible for clean up idle self-managed associations.
+ *
  * The job will be executed only if the device is charging and in idle mode due to the application
- * will be killed if association/role are revoked.
+ * will be killed if association/role are revoked. See {@link AssociationRevokeProcessor}
  */
 public class InactiveAssociationsRemovalService extends JobService {
+
+    private static final String TAG = "CDM_InactiveAssociationsRemovalService";
     private static final String JOB_NAMESPACE = "companion";
     private static final int JOB_ID = 1;
     private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1);
@@ -60,7 +62,10 @@
         return false;
     }
 
-    static void schedule(Context context) {
+    /**
+     * Schedule this job.
+     */
+    public static void schedule(Context context) {
         Slog.i(TAG, "Scheduling the Association Removal job");
         final JobScheduler jobScheduler =
                 context.getSystemService(JobScheduler.class).forNamespace(JOB_NAMESPACE);
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index 74236a4..a08e0da 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -52,8 +52,8 @@
 import android.util.Slog;
 
 import com.android.internal.R;
-import com.android.server.companion.AssociationStore;
 import com.android.server.companion.CompanionDeviceManagerService;
+import com.android.server.companion.association.AssociationStore;
 import com.android.server.companion.transport.CompanionTransportManager;
 import com.android.server.companion.utils.PackageUtils;
 import com.android.server.companion.utils.PermissionsUtils;
diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
index 2899c05..99466a9 100644
--- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
+++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
@@ -59,8 +59,8 @@
 import android.util.Log;
 import android.util.Slog;
 
-import com.android.server.companion.AssociationStore;
-import com.android.server.companion.AssociationStore.ChangeType;
+import com.android.server.companion.association.AssociationStore;
+import com.android.server.companion.association.AssociationStore.ChangeType;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
index 0287f62..4da3f9b 100644
--- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
+++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
@@ -39,7 +39,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
-import com.android.server.companion.AssociationStore;
+import com.android.server.companion.association.AssociationStore;
 
 import java.util.Arrays;
 import java.util.Collections;
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index 3da9693..37bbb93 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -44,7 +44,7 @@
 import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.companion.AssociationStore;
+import com.android.server.companion.association.AssociationStore;
 
 import java.io.PrintWriter;
 import java.util.HashSet;
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 3861f99..6dd14ac 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -32,7 +32,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.companion.AssociationStore;
+import com.android.server.companion.association.AssociationStore;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
diff --git a/services/companion/java/com/android/server/companion/utils/AssociationUtils.java b/services/companion/java/com/android/server/companion/utils/AssociationUtils.java
new file mode 100644
index 0000000..e4d9641
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/utils/AssociationUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.utils;
+
+import android.annotation.UserIdInt;
+
+public final class AssociationUtils {
+
+    /** Range of Association IDs allocated for a user. */
+    private static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000;
+
+    /**
+     * Get the left boundary of the association id range for the user.
+     */
+    public static int getFirstAssociationIdForUser(@UserIdInt int userId) {
+        // We want the IDs to start from 1, not 0.
+        return userId * ASSOCIATIONS_IDS_PER_USER_RANGE + 1;
+    }
+
+    /**
+     * Get the right boundary of the association id range for the user.
+     */
+    public static int getLastAssociationIdForUser(@UserIdInt int userId) {
+        return (userId + 1) * ASSOCIATIONS_IDS_PER_USER_RANGE;
+    }
+
+    private AssociationUtils() {}
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 6d731b2..b1672ed 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -210,7 +210,7 @@
                     mActivityListener.onTopActivityChanged(displayId, topActivity,
                             UserHandle.USER_NULL);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Unable to call mActivityListener", e);
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
 
@@ -220,7 +220,7 @@
                 try {
                     mActivityListener.onTopActivityChanged(displayId, topActivity, userId);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Unable to call mActivityListener", e);
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
 
@@ -229,7 +229,7 @@
                 try {
                     mActivityListener.onDisplayEmpty(displayId);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Unable to call mActivityListener", e);
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
         };
@@ -1213,7 +1213,7 @@
         mContext.startActivityAsUser(
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK),
                 ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(),
-                mContext.getUser());
+                UserHandle.SYSTEM);
     }
 
     private void onSecureWindowShown(int displayId, int uid) {
diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java
index f82a6aa..748253f 100644
--- a/services/core/java/com/android/server/DropBoxManagerService.java
+++ b/services/core/java/com/android/server/DropBoxManagerService.java
@@ -106,7 +106,7 @@
     private static final int DEFAULT_AGE_SECONDS = 3 * 86400;
     private static final int DEFAULT_MAX_FILES = 1000;
     private static final int DEFAULT_MAX_FILES_LOWRAM = 300;
-    private static final int DEFAULT_QUOTA_KB = 10 * 1024;
+    private static final int DEFAULT_QUOTA_KB = Build.IS_USERDEBUG ? 20 * 1024 : 10 * 1024;
     private static final int DEFAULT_QUOTA_PERCENT = 10;
     private static final int DEFAULT_RESERVE_PERCENT = 0;
     private static final int QUOTA_RESCAN_MILLIS = 5000;
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index e1d7be1..1a3ef73 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -264,8 +264,8 @@
     final ArrayMap<String, ArraySet<String>> mAllowIgnoreLocationSettings = new ArrayMap<>();
 
     // These are the packages that are allow-listed to be able to access camera when
-    // the camera privacy state is for driver assistance apps only.
-    final ArrayMap<String, Boolean> mAllowlistCameraPrivacy = new ArrayMap<>();
+    // the camera privacy state is enabled.
+    final ArraySet<String> mAllowlistCameraPrivacy = new ArraySet<>();
 
     // These are the action strings of broadcasts which are whitelisted to
     // be delivered anonymously even to apps which target O+.
@@ -489,7 +489,7 @@
         return mAllowedAssociations;
     }
 
-    public ArrayMap<String, Boolean> getCameraPrivacyAllowlist() {
+    public ArraySet<String> getCameraPrivacyAllowlist() {
         return mAllowlistCameraPrivacy;
     }
 
@@ -1076,13 +1076,11 @@
                     case "camera-privacy-allowlisted-app" : {
                         if (allowOverrideAppRestrictions) {
                             String pkgname = parser.getAttributeValue(null, "package");
-                            boolean isMandatory = XmlUtils.readBooleanAttribute(
-                                    parser, "mandatory", false);
                             if (pkgname == null) {
                                 Slog.w(TAG, "<" + name + "> without package in "
                                         + permFile + " at " + parser.getPositionDescription());
                             } else {
-                                mAllowlistCameraPrivacy.put(pkgname, isMandatory);
+                                mAllowlistCameraPrivacy.add(pkgname);
                             }
                         } else {
                             logNotAllowedInPartition(name, permFile, parser);
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index 8dc15ad..25337a4 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -100,7 +100,7 @@
             "file_patterns": ["VcnManagementService\\.java"]
         },
         {
-            "name": "FrameworksNetTests",
+            "name": "FrameworksVpnTests",
             "options": [
                 {
                     "exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -163,9 +163,6 @@
                 }
             ],
             "file_patterns": ["PinnerService\\.java"]
-        },
-        {
-            "name": "FrameworksVpnTests"
         }
     ]
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 663ba8a..5e36709 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -141,7 +141,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKUP;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_BACKGROUND;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_NETWORK;
@@ -726,29 +725,19 @@
     // Whether we should use SCHED_FIFO for UI and RenderThreads.
     final boolean mUseFifoUiScheduling;
 
-    // Use an offload queue for long broadcasts, e.g. BOOT_COMPLETED.
-    // For simplicity, since we statically declare the size of the array of BroadcastQueues,
-    // we still create this new offload queue, but never ever put anything on it.
-    final boolean mEnableOffloadQueue;
-
     /**
      * Flag indicating if we should use {@link BroadcastQueueModernImpl} instead
      * of the default {@link BroadcastQueueImpl}.
      */
     final boolean mEnableModernQueue;
 
-    static final int BROADCAST_QUEUE_FG = 0;
-    static final int BROADCAST_QUEUE_BG = 1;
-    static final int BROADCAST_QUEUE_BG_OFFLOAD = 2;
-    static final int BROADCAST_QUEUE_FG_OFFLOAD = 3;
-
     @GuardedBy("this")
     private final SparseArray<IUnsafeIntentStrictModeCallback>
             mStrictModeCallbacks = new SparseArray<>();
 
     // Convenient for easy iteration over the queues. Foreground is first
     // so that dispatch of foreground broadcasts gets precedence.
-    final BroadcastQueue[] mBroadcastQueues;
+    private BroadcastQueue mBroadcastQueue;
 
     @GuardedBy("this")
     BroadcastStats mLastBroadcastStats;
@@ -758,43 +747,6 @@
 
     TraceErrorLogger mTraceErrorLogger;
 
-    BroadcastQueue broadcastQueueForIntent(Intent intent) {
-        return broadcastQueueForFlags(intent.getFlags(), intent);
-    }
-
-    BroadcastQueue broadcastQueueForFlags(int flags) {
-        return broadcastQueueForFlags(flags, null);
-    }
-
-    BroadcastQueue broadcastQueueForFlags(int flags, Object cookie) {
-        if (mEnableModernQueue) {
-            return mBroadcastQueues[0];
-        }
-
-        if (isOnFgOffloadQueue(flags)) {
-            if (DEBUG_BROADCAST_BACKGROUND) {
-                Slog.i(TAG_BROADCAST,
-                        "Broadcast intent " + cookie + " on foreground offload queue");
-            }
-            return mBroadcastQueues[BROADCAST_QUEUE_FG_OFFLOAD];
-        }
-
-        if (isOnBgOffloadQueue(flags)) {
-            if (DEBUG_BROADCAST_BACKGROUND) {
-                Slog.i(TAG_BROADCAST,
-                        "Broadcast intent " + cookie + " on background offload queue");
-            }
-            return mBroadcastQueues[BROADCAST_QUEUE_BG_OFFLOAD];
-        }
-
-        final boolean isFg = (flags & Intent.FLAG_RECEIVER_FOREGROUND) != 0;
-        if (DEBUG_BROADCAST_BACKGROUND) Slog.i(TAG_BROADCAST,
-                "Broadcast intent " + cookie + " on "
-                + (isFg ? "foreground" : "background") + " queue");
-        return (isFg) ? mBroadcastQueues[BROADCAST_QUEUE_FG]
-                : mBroadcastQueues[BROADCAST_QUEUE_BG];
-    }
-
     private volatile int mDeviceOwnerUid = INVALID_UID;
 
     /**
@@ -2556,9 +2508,8 @@
         mInternal = new LocalService();
         mPendingStartActivityUids = new PendingStartActivityUids();
         mUseFifoUiScheduling = false;
-        mEnableOffloadQueue = false;
         mEnableModernQueue = false;
-        mBroadcastQueues = injector.getBroadcastQueues(this);
+        mBroadcastQueue = injector.getBroadcastQueue(this);
         mComponentAliasResolver = new ComponentAliasResolver(this);
     }
 
@@ -2599,12 +2550,10 @@
                 ? new OomAdjusterModernImpl(this, mProcessList, activeUids)
                 : new OomAdjuster(this, mProcessList, activeUids);
 
-        mEnableOffloadQueue = SystemProperties.getBoolean(
-                "persist.device_config.activity_manager_native_boot.offload_queue_enabled", true);
         mEnableModernQueue = new BroadcastConstants(
                 Settings.Global.BROADCAST_FG_CONSTANTS).MODERN_QUEUE_ENABLED;
 
-        mBroadcastQueues = mInjector.getBroadcastQueues(this);
+        mBroadcastQueue = mInjector.getBroadcastQueue(this);
 
         mServices = new ActiveServices(this);
         mCpHelper = new ContentProviderHelper(this, true);
@@ -2671,6 +2620,14 @@
         mComponentAliasResolver = new ComponentAliasResolver(this);
     }
 
+    void setBroadcastQueueForTest(BroadcastQueue broadcastQueue) {
+        mBroadcastQueue = broadcastQueue;
+    }
+
+    BroadcastQueue getBroadcastQueue() {
+        return mBroadcastQueue;
+    }
+
     public void setSystemServiceManager(SystemServiceManager mgr) {
         mSystemServiceManager = mgr;
     }
@@ -4280,19 +4237,14 @@
         }
 
         // Clean-up disabled broadcast receivers.
-        for (int i = mBroadcastQueues.length - 1; i >= 0; i--) {
-            mBroadcastQueues[i].cleanupDisabledPackageReceiversLocked(
-                    packageName, disabledClasses, userId);
-        }
+        mBroadcastQueue.cleanupDisabledPackageReceiversLocked(
+                packageName, disabledClasses, userId);
 
     }
 
     final boolean clearBroadcastQueueForUserLocked(int userId) {
-        boolean didSomething = false;
-        for (int i = mBroadcastQueues.length - 1; i >= 0; i--) {
-            didSomething |= mBroadcastQueues[i].cleanupDisabledPackageReceiversLocked(
-                    null, null, userId);
-        }
+        boolean didSomething = mBroadcastQueue.cleanupDisabledPackageReceiversLocked(
+                null, null, userId);
         return didSomething;
     }
 
@@ -4445,10 +4397,8 @@
         mUgmInternal.removeUriPermissionsForPackage(packageName, userId, false, false);
 
         if (doit) {
-            for (i = mBroadcastQueues.length - 1; i >= 0; i--) {
-                didSomething |= mBroadcastQueues[i].cleanupDisabledPackageReceiversLocked(
+            didSomething |= mBroadcastQueue.cleanupDisabledPackageReceiversLocked(
                         packageName, null, userId);
-            }
         }
 
         if (packageName == null || uninstalling || packageStateStopped) {
@@ -4515,9 +4465,7 @@
                 // Take care of any services that are waiting for the process.
                 mServices.processStartTimedOutLocked(app);
                 // Take care of any broadcasts waiting for the process.
-                for (BroadcastQueue queue : mBroadcastQueues) {
-                    queue.onApplicationTimeoutLocked(app);
-                }
+                mBroadcastQueue.onApplicationTimeoutLocked(app);
                 if (!isKillTimeout) {
                     mBatteryStatsService.noteProcessFinish(app.processName, app.info.uid);
                     app.killLocked("start timeout",
@@ -4779,36 +4727,47 @@
                 // being bound to an application.
                 thread.runIsolatedEntryPoint(
                         app.getIsolatedEntryPoint(), app.getIsolatedEntryPointArgs());
-            } else if (instr2 != null) {
-                thread.bindApplication(processName, appInfo,
-                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
-                        instr2.mIsSdkInSandbox,
-                        providerList,
-                        instr2.mClass,
-                        profilerInfo, instr2.mArguments,
-                        instr2.mWatcher,
-                        instr2.mUiAutomationConnection, testMode,
-                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
-                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
-                        new Configuration(app.getWindowProcessController().getConfiguration()),
-                        app.getCompat(), getCommonServicesLocked(app.isolated),
-                        mCoreSettingsObserver.getCoreSettingsLocked(),
-                        buildSerial, autofillOptions, contentCaptureOptions,
-                        app.getDisabledCompatChanges(), serializedSystemFontMap,
-                        app.getStartElapsedTime(), app.getStartUptime());
             } else {
-                thread.bindApplication(processName, appInfo,
-                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
-                        /* isSdkInSandbox= */ false,
-                        providerList, null, profilerInfo, null, null, null, testMode,
-                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
-                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
+                boolean isSdkInSandbox = false;
+                ComponentName instrumentationName = null;
+                Bundle instrumentationArgs = null;
+                IInstrumentationWatcher instrumentationWatcher = null;
+                IUiAutomationConnection instrumentationUiConnection = null;
+                if (instr2 != null) {
+                    isSdkInSandbox = instr2.mIsSdkInSandbox;
+                    instrumentationName = instr2.mClass;
+                    instrumentationArgs = instr2.mArguments;
+                    instrumentationWatcher = instr2.mWatcher;
+                    instrumentationUiConnection = instr2.mUiAutomationConnection;
+                }
+                thread.bindApplication(
+                        processName,
+                        appInfo,
+                        app.sdkSandboxClientAppVolumeUuid,
+                        app.sdkSandboxClientAppPackage,
+                        isSdkInSandbox,
+                        providerList,
+                        instrumentationName,
+                        profilerInfo,
+                        instrumentationArgs,
+                        instrumentationWatcher,
+                        instrumentationUiConnection,
+                        testMode,
+                        mBinderTransactionTrackingEnabled,
+                        enableTrackAllocation,
+                        isRestrictedBackupMode || !normalMode,
+                        app.isPersistent(),
                         new Configuration(app.getWindowProcessController().getConfiguration()),
-                        app.getCompat(), getCommonServicesLocked(app.isolated),
+                        app.getCompat(),
+                        getCommonServicesLocked(app.isolated),
                         mCoreSettingsObserver.getCoreSettingsLocked(),
-                        buildSerial, autofillOptions, contentCaptureOptions,
-                        app.getDisabledCompatChanges(), serializedSystemFontMap,
-                        app.getStartElapsedTime(), app.getStartUptime());
+                        buildSerial,
+                        autofillOptions,
+                        contentCaptureOptions,
+                        app.getDisabledCompatChanges(),
+                        serializedSystemFontMap,
+                        app.getStartElapsedTime(),
+                        app.getStartUptime());
             }
 
             Message msg = mHandler.obtainMessage(BIND_APPLICATION_TIMEOUT_SOFT_MSG);
@@ -4948,9 +4907,7 @@
             // Check if a next-broadcast receiver is in this process...
             if (!badApp) {
                 try {
-                    for (BroadcastQueue queue : mBroadcastQueues) {
-                        didSomething |= queue.onApplicationAttachedLocked(app);
-                    }
+                    didSomething |= mBroadcastQueue.onApplicationAttachedLocked(app);
                     checkTime(startTime, "finishAttachApplicationInner: "
                             + "after dispatching broadcasts");
                 } catch (BroadcastDeliveryFailedException e) {
@@ -9090,9 +9047,7 @@
     }
 
     private void startBroadcastObservers() {
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.start(mContext.getContentResolver());
-        }
+        mBroadcastQueue.start(mContext.getContentResolver());
     }
 
     private void updateForceBackgroundCheck(boolean enabled) {
@@ -9363,7 +9318,9 @@
                 sb.append("Animations-Running: ").append(info.numAnimationsRunning).append("\n");
             }
             if (info.broadcastIntentAction != null) {
-                sb.append("Broadcast-Intent-Action: ").append(info.broadcastIntentAction).append("\n");
+                sb.append("Broadcast-Intent-Action: ")
+                        .append(info.broadcastIntentAction)
+                        .append("\n");
             }
             if (info.durationMillis != -1) {
                 sb.append("Duration-Millis: ").append(info.durationMillis).append("\n");
@@ -9723,82 +9680,126 @@
         // If process is null, we are being called from some internal code
         // and may be about to die -- run this synchronously.
         final boolean runSynchronously = process == null;
-        Thread worker = new Thread("Error dump: " + dropboxTag) {
-            @Override
-            public void run() {
-                if (report != null) {
-                    sb.append(report);
-                }
-
-                String logcatSetting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag;
-                String maxBytesSetting = Settings.Global.MAX_ERROR_BYTES_PREFIX + dropboxTag;
-                int lines = Build.IS_USER
-                        ? 0
-                        : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0);
-                int dropboxMaxSize = Settings.Global.getInt(
-                        mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE);
-
-                if (dataFile != null) {
-                    // Attach the stack traces file to the report so collectors can load them
-                    // by file if they have access.
-                    sb.append(DATA_FILE_PATH_HEADER)
-                            .append(dataFile.getAbsolutePath()).append('\n');
-
-                    int maxDataFileSize = dropboxMaxSize
-                            - sb.length()
-                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
-                            - DATA_FILE_PATH_FOOTER.length();
-
-                    if (maxDataFileSize > 0) {
-                        // Inline dataFile contents if there is room.
-                        try {
-                            sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
-                                    "\n\n[[TRUNCATED]]\n"));
-                        } catch (IOException e) {
-                            Slog.e(TAG, "Error reading " + dataFile, e);
+        Thread worker =
+                new Thread("Error dump: " + dropboxTag) {
+                    @Override
+                    public void run() {
+                        if (report != null) {
+                            sb.append(report);
                         }
+
+                        String logcatSetting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag;
+                        String maxBytesSetting =
+                                Settings.Global.MAX_ERROR_BYTES_PREFIX + dropboxTag;
+                        int lines =
+                                Build.IS_USER
+                                        ? 0
+                                        : Settings.Global.getInt(
+                                                mContext.getContentResolver(), logcatSetting, 0);
+                        int dropboxMaxSize =
+                                Settings.Global.getInt(
+                                        mContext.getContentResolver(),
+                                        maxBytesSetting,
+                                        DROPBOX_DEFAULT_MAX_SIZE);
+
+                        if (dataFile != null) {
+                            // Attach the stack traces file to the report so collectors can load
+                            // them
+                            // by file if they have access.
+                            sb.append(DATA_FILE_PATH_HEADER)
+                                    .append(dataFile.getAbsolutePath())
+                                    .append('\n');
+
+                            int maxDataFileSize =
+                                    dropboxMaxSize
+                                            - sb.length()
+                                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
+                                            - DATA_FILE_PATH_FOOTER.length();
+
+                            if (maxDataFileSize > 0) {
+                                // Inline dataFile contents if there is room.
+                                try {
+                                    sb.append(
+                                            FileUtils.readTextFile(
+                                                    dataFile,
+                                                    maxDataFileSize,
+                                                    "\n\n[[TRUNCATED]]\n"));
+                                } catch (IOException e) {
+                                    Slog.e(TAG, "Error reading " + dataFile, e);
+                                }
+                            }
+
+                            // Always append the footer, even there wasn't enough space to inline
+                            // the
+                            // dataFile contents.
+                            sb.append(DATA_FILE_PATH_FOOTER);
+                        }
+
+                        if (crashInfo != null && crashInfo.stackTrace != null) {
+                            sb.append(crashInfo.stackTrace);
+                        }
+
+                        if (lines > 0 && !runSynchronously) {
+                            sb.append("\n");
+
+                            InputStreamReader input = null;
+                            try {
+                                java.lang.Process logcat =
+                                        new ProcessBuilder(
+                                                        // Time out after 10s of inactivity, but
+                                                        // kill logcat with SEGV
+                                                        // so we can investigate why it didn't
+                                                        // finish.
+                                                        "/system/bin/timeout",
+                                                        "-i",
+                                                        "-s",
+                                                        "SEGV",
+                                                        "10s",
+                                                        // Merge several logcat streams, and take
+                                                        // the last N lines.
+                                                        "/system/bin/logcat",
+                                                        "-v",
+                                                        "threadtime",
+                                                        "-b",
+                                                        "events",
+                                                        "-b",
+                                                        "system",
+                                                        "-b",
+                                                        "main",
+                                                        "-b",
+                                                        "crash",
+                                                        "-t",
+                                                        String.valueOf(lines))
+                                                .redirectErrorStream(true)
+                                                .start();
+
+                                try {
+                                    logcat.getOutputStream().close();
+                                } catch (IOException e) {
+                                }
+                                try {
+                                    logcat.getErrorStream().close();
+                                } catch (IOException e) {
+                                }
+                                input = new InputStreamReader(logcat.getInputStream());
+
+                                int num;
+                                char[] buf = new char[8192];
+                                while ((num = input.read(buf)) > 0) sb.append(buf, 0, num);
+                            } catch (IOException e) {
+                                Slog.e(TAG, "Error running logcat", e);
+                            } finally {
+                                if (input != null)
+                                    try {
+                                        input.close();
+                                    } catch (IOException e) {
+                                    }
+                            }
+                        }
+
+                        dbox.addText(dropboxTag, sb.toString());
                     }
-
-                    // Always append the footer, even there wasn't enough space to inline the
-                    // dataFile contents.
-                    sb.append(DATA_FILE_PATH_FOOTER);
-                }
-
-                if (crashInfo != null && crashInfo.stackTrace != null) {
-                    sb.append(crashInfo.stackTrace);
-                }
-
-                if (lines > 0 && !runSynchronously) {
-                    sb.append("\n");
-
-                    InputStreamReader input = null;
-                    try {
-                        java.lang.Process logcat = new ProcessBuilder(
-                                // Time out after 10s of inactivity, but kill logcat with SEGV
-                                // so we can investigate why it didn't finish.
-                                "/system/bin/timeout", "-i", "-s", "SEGV", "10s",
-                                // Merge several logcat streams, and take the last N lines.
-                                "/system/bin/logcat", "-v", "threadtime", "-b", "events", "-b", "system",
-                                "-b", "main", "-b", "crash", "-t", String.valueOf(lines))
-                                        .redirectErrorStream(true).start();
-
-                        try { logcat.getOutputStream().close(); } catch (IOException e) {}
-                        try { logcat.getErrorStream().close(); } catch (IOException e) {}
-                        input = new InputStreamReader(logcat.getInputStream());
-
-                        int num;
-                        char[] buf = new char[8192];
-                        while ((num = input.read(buf)) > 0) sb.append(buf, 0, num);
-                    } catch (IOException e) {
-                        Slog.e(TAG, "Error running logcat", e);
-                    } finally {
-                        if (input != null) try { input.close(); } catch (IOException e) {}
-                    }
-                }
-
-                dbox.addText(dropboxTag, sb.toString());
-            }
-        };
+                };
 
         if (runSynchronously) {
             final int oldMask = StrictMode.allowThreadDiskWritesMask();
@@ -10166,43 +10167,48 @@
             mOomAdjuster.dumpCacheOomRankerSettings(pw);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
-
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpAllowedAssociationsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
-
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mPendingIntentController.dumpPendingIntents(pw, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpBroadcastsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             if (dumpAll || dumpPackage != null) {
                 dumpBroadcastStatsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
             }
             mCpHelper.dumpProvidersLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpPermissions(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             sdumper = mServices.newServiceDumperLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             if (!dumpClient) {
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 sdumper.dumpLocked();
             }
@@ -10217,7 +10223,8 @@
         // method with the lock held.
         if (dumpClient) {
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             sdumper.dumpWithClient();
         }
@@ -10230,33 +10237,38 @@
             // proxies in the first place.
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpBinderProxies(pw, BINDER_PROXY_HIGH_WATERMARK /* minToDump */);
         }
         synchronized(this) {
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mAtmInternal.dump(DUMP_RECENTS_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                     dumpPackage, displayIdFilter);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mAtmInternal.dump(DUMP_LASTANR_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                     dumpPackage, displayIdFilter);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mAtmInternal.dump(DUMP_STARTER_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                     dumpPackage, displayIdFilter);
             if (dumpPackage == null) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 mAtmInternal.dump(DUMP_CONTAINERS_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                         dumpPackage, displayIdFilter);
@@ -10266,7 +10278,8 @@
             if (!dumpNormalPriority) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 mAtmInternal.dump(DUMP_ACTIVITIES_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                         dumpPackage, displayIdFilter);
@@ -10274,45 +10287,53 @@
             if (mAssociations.size() > 0) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 dumpAssociationsLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
             }
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
                 mProcessList.getAppStartInfoTracker().dumpHistoryProcessStartInfo(pw, dumpPackage);
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
                 mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage);
             }
             if (dumpPackage == null) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 mOomAdjProfiler.dump(pw);
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 dumpLmkLocked(pw);
             }
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             synchronized (mProcLock) {
                 mProcessList.dumpProcessesLSP(fd, pw, args, opti, dumpAll, dumpPackage, dumpAppId);
             }
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpUsers(pw);
 
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mComponentAliasResolver.dump(pw);
         }
@@ -10322,7 +10343,8 @@
      * Dump the app restriction controller, it's required not to hold the global lock here.
      */
     private void dumpAppRestrictionController(PrintWriter pw) {
-        pw.println("-------------------------------------------------------------------------------");
+        pw.println(
+                "-------------------------------------------------------------------------------");
         mAppRestrictionController.dump(pw, "");
     }
 
@@ -11431,9 +11453,7 @@
             }
         }
         mReceiverResolver.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.RECEIVER_RESOLVER);
-        for (BroadcastQueue q : mBroadcastQueues) {
-            q.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.BROADCAST_QUEUE);
-        }
+        mBroadcastQueue.dumpDebug(proto, ActivityManagerServiceDumpBroadcastsProto.BROADCAST_QUEUE);
         synchronized (mStickyBroadcasts) {
             for (int user = 0; user < mStickyBroadcasts.size(); user++) {
                 long token = proto.start(
@@ -11462,7 +11482,9 @@
 
     void dumpAllowedAssociationsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
             int opti, boolean dumpAll, String dumpPackage) {
-        pw.println("ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity allowed-associations)");
+        pw.println(
+                "ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity"
+                    + " allowed-associations)");
         boolean printed = false;
         if (mAllowedAssociations != null) {
             for (int i = 0; i < mAllowedAssociations.size(); i++) {
@@ -11581,11 +11603,9 @@
         }
 
         if (!onlyReceivers) {
-            for (BroadcastQueue q : mBroadcastQueues) {
-                needSep = q.dumpLocked(fd, pw, args, opti,
-                        dumpConstants, dumpHistory, dumpAll, dumpPackage, needSep);
-                printedAnything |= needSep;
-            }
+            needSep = mBroadcastQueue.dumpLocked(fd, pw, args, opti,
+                    dumpConstants, dumpHistory, dumpAll, dumpPackage, needSep);
+            printedAnything |= needSep;
         }
 
         needSep = true;
@@ -11641,9 +11661,8 @@
 
         if (!onlyHistory && !onlyReceivers && dumpAll) {
             pw.println();
-            for (BroadcastQueue queue : mBroadcastQueues) {
-                pw.println("  Queue " + queue.toString() + ": " + queue.describeStateLocked());
-            }
+            pw.println("  Queue " + mBroadcastQueue.toString() + ": "
+                    + mBroadcastQueue.describeStateLocked());
             pw.println("  mHandler:");
             mHandler.dump(new PrintWriterPrinter(pw), "    ");
             needSep = true;
@@ -12816,7 +12835,8 @@
             if (!opts.isCompact) {
                 pw.print(" Used RAM: "); pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss
                         + kernelUsed)); pw.print(" (");
-                pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss)); pw.print(" used pss + ");
+                pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss));
+                pw.print(" used pss + ");
                 pw.print(stringifyKBSize(kernelUsed)); pw.print(" kernel)\n");
                 pw.print(" Lost RAM: "); pw.println(stringifyKBSize(lostRAM));
             } else {
@@ -13490,9 +13510,7 @@
             mOomAdjuster.mCachedAppOptimizer.onCleanupApplicationRecordLocked(app);
         }
         mAppProfiler.onCleanupApplicationRecordLocked(app);
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.onApplicationCleanupLocked(app);
-        }
+        mBroadcastQueue.onApplicationCleanupLocked(app);
         clearProcessForegroundLocked(app);
         mServices.killServicesLocked(app, allowRestart);
         mPhantomProcessList.onAppDied(pid);
@@ -13686,8 +13704,15 @@
         }
         validateServiceInstanceName(instanceName);
 
-        if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
-                "*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground);
+        if (DEBUG_SERVICE)
+            Slog.v(
+                    TAG_SERVICE,
+                    "*** startService: "
+                            + service
+                            + " type="
+                            + resolvedType
+                            + " fg="
+                            + requireForeground);
         final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
@@ -14599,7 +14624,7 @@
                             originalStickyCallingUid))) {
                         sticky = broadcast.intent;
                     }
-                    BroadcastQueue queue = broadcastQueueForIntent(broadcast.intent);
+                    BroadcastQueue queue = mBroadcastQueue;
                     BroadcastRecord r = new BroadcastRecord(queue, broadcast.intent, null,
                             null, null, -1, -1, false, null, null, null, null, OP_NONE,
                             BroadcastOptions.makeWithDeferUntilActive(broadcast.deferUntilActive),
@@ -15448,9 +15473,14 @@
             if (checkPermission(android.Manifest.permission.BROADCAST_STICKY,
                     callingPid, callingUid)
                     != PackageManager.PERMISSION_GRANTED) {
-                String msg = "Permission Denial: broadcastIntent() requesting a sticky broadcast from pid="
-                        + callingPid + ", uid=" + callingUid
-                        + " requires " + android.Manifest.permission.BROADCAST_STICKY;
+                String msg =
+                        "Permission Denial: broadcastIntent() requesting a sticky broadcast from"
+                            + " pid="
+                                + callingPid
+                                + ", uid="
+                                + callingUid
+                                + " requires "
+                                + android.Manifest.permission.BROADCAST_STICKY;
                 Slog.w(TAG, msg);
                 throw new SecurityException(msg);
             }
@@ -15593,7 +15623,7 @@
                 checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
                         isProtectedBroadcast, registeredReceivers);
             }
-            final BroadcastQueue queue = broadcastQueueForIntent(intent);
+            final BroadcastQueue queue = mBroadcastQueue;
             BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
@@ -15686,7 +15716,7 @@
 
         if ((receivers != null && receivers.size() > 0)
                 || resultTo != null) {
-            BroadcastQueue queue = broadcastQueueForIntent(intent);
+            BroadcastQueue queue = mBroadcastQueue;
             filterNonExportedComponents(intent, callingUid, callingPid, receivers,
                     mPlatformCompat, callerPackage, resolvedType);
             BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
@@ -15974,9 +16004,7 @@
     }
 
     void backgroundServicesFinishedLocked(int userId) {
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.backgroundServicesFinishedLocked(userId);
-        }
+        mBroadcastQueue.backgroundServicesFinishedLocked(userId);
     }
 
     public void finishReceiver(IBinder caller, int resultCode, String resultData,
@@ -15997,8 +16025,7 @@
                     return;
                 }
 
-                final BroadcastQueue queue = broadcastQueueForFlags(flags);
-                queue.finishReceiverLocked(callerApp, resultCode,
+                mBroadcastQueue.finishReceiverLocked(callerApp, resultCode,
                         resultData, resultExtras, resultAbort, true);
                 // updateOomAdjLocked() will be done here
                 trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER);
@@ -16589,10 +16616,7 @@
     // =========================================================
 
     boolean isReceivingBroadcastLocked(ProcessRecord app, int[] outSchedGroup) {
-        int res = ProcessList.SCHED_GROUP_UNDEFINED;
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            res = Math.max(res, queue.getPreferredSchedulingGroupLocked(app));
-        }
+        final int res = mBroadcastQueue.getPreferredSchedulingGroupLocked(app);
         outSchedGroup[0] = res;
         return res != ProcessList.SCHED_GROUP_UNDEFINED;
     }
@@ -16716,10 +16740,8 @@
      */
     @GuardedBy("this")
     final boolean canGcNowLocked() {
-        for (BroadcastQueue q : mBroadcastQueues) {
-            if (!q.isIdleLocked()) {
-                return false;
-            }
+        if (!mBroadcastQueue.isIdleLocked()) {
+            return false;
         }
         return mAtmInternal.canGcNow();
     }
@@ -17929,9 +17951,7 @@
     }
 
     void onProcessFreezableChangedLocked(ProcessRecord app) {
-        if (mEnableModernQueue) {
-            mBroadcastQueues[0].onProcessFreezableChangedLocked(app);
-        }
+        mBroadcastQueue.onProcessFreezableChangedLocked(app);
     }
 
     @VisibleForTesting
@@ -19622,9 +19642,7 @@
         if (flushBroadcastLoopers) {
             BroadcastLoopers.waitForIdle(pw);
         }
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.waitForIdle(pw);
-        }
+        mBroadcastQueue.waitForIdle(pw);
         pw.println("All broadcast queues are idle!");
         pw.flush();
     }
@@ -19640,9 +19658,7 @@
         if (flushBroadcastLoopers) {
             BroadcastLoopers.waitForBarrier(pw);
         }
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.waitForBarrier(pw);
-        }
+        mBroadcastQueue.waitForBarrier(pw);
         if (flushApplicationThreads) {
             waitForApplicationBarrier(pw);
         }
@@ -19718,9 +19734,7 @@
 
     void waitForBroadcastDispatch(@NonNull PrintWriter pw, @NonNull Intent intent) {
         enforceCallingPermission(permission.DUMP, "waitForBroadcastDispatch");
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.waitForDispatched(intent, pw);
-        }
+        mBroadcastQueue.waitForDispatched(intent, pw);
     }
 
     void setIgnoreDeliveryGroupPolicy(@NonNull String broadcastAction) {
@@ -19765,9 +19779,7 @@
             return;
         }
 
-        for (BroadcastQueue queue : mBroadcastQueues) {
-            queue.forceDelayBroadcastDelivery(targetPackage, delayedDurationMs);
-        }
+        mBroadcastQueue.forceDelayBroadcastDelivery(targetPackage, delayedDurationMs);
     }
 
     @Override
@@ -20321,7 +20333,7 @@
             return mNmi != null;
         }
 
-        public BroadcastQueue[] getBroadcastQueues(ActivityManagerService service) {
+        public BroadcastQueue getBroadcastQueue(ActivityManagerService service) {
             // Broadcast policy parameters
             final BroadcastConstants foreConstants = new BroadcastConstants(
                     Settings.Global.BROADCAST_FG_CONSTANTS);
@@ -20337,26 +20349,8 @@
             // by default, no "slow" policy in this queue
             offloadConstants.SLOW_TIME = Integer.MAX_VALUE;
 
-            final BroadcastQueue[] broadcastQueues;
-            final Handler handler = service.mHandler;
-            if (service.mEnableModernQueue) {
-                broadcastQueues = new BroadcastQueue[1];
-                broadcastQueues[0] = new BroadcastQueueModernImpl(service, handler,
+            return new BroadcastQueueModernImpl(service, service.mHandler,
                         foreConstants, backConstants);
-            } else {
-                broadcastQueues = new BroadcastQueue[4];
-                broadcastQueues[BROADCAST_QUEUE_FG] = new BroadcastQueueImpl(service, handler,
-                        "foreground", foreConstants, false, ProcessList.SCHED_GROUP_DEFAULT);
-                broadcastQueues[BROADCAST_QUEUE_BG] = new BroadcastQueueImpl(service, handler,
-                        "background", backConstants, true, ProcessList.SCHED_GROUP_BACKGROUND);
-                broadcastQueues[BROADCAST_QUEUE_BG_OFFLOAD] = new BroadcastQueueImpl(service,
-                        handler, "offload_bg", offloadConstants, true,
-                        ProcessList.SCHED_GROUP_BACKGROUND);
-                broadcastQueues[BROADCAST_QUEUE_FG_OFFLOAD] = new BroadcastQueueImpl(service,
-                        handler, "offload_fg", foreConstants, true,
-                        ProcessList.SCHED_GROUP_BACKGROUND);
-            }
-            return broadcastQueues;
         }
 
         /** @see Binder#getCallingUid */
@@ -20678,14 +20672,6 @@
         }
     }
 
-    private boolean isOnFgOffloadQueue(int flags) {
-        return ((flags & Intent.FLAG_RECEIVER_OFFLOAD_FOREGROUND) != 0);
-    }
-
-    private boolean isOnBgOffloadQueue(int flags) {
-        return (mEnableOffloadQueue && ((flags & Intent.FLAG_RECEIVER_OFFLOAD) != 0));
-    }
-
     @Override
     public ParcelFileDescriptor getLifeMonitor() {
         if (!isCallerShell()) {
diff --git a/services/core/java/com/android/server/am/BroadcastDispatcher.java b/services/core/java/com/android/server/am/BroadcastDispatcher.java
deleted file mode 100644
index 8aa3921..0000000
--- a/services/core/java/com/android/server/am/BroadcastDispatcher.java
+++ /dev/null
@@ -1,1266 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.am;
-
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_DEFERRAL;
-import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_NONE;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UptimeMillisLong;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.os.UserHandle;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
-import android.util.proto.ProtoOutputStream;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.AlarmManagerInternal;
-import com.android.server.LocalServices;
-
-import dalvik.annotation.optimization.NeverCompile;
-
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Set;
-
-/**
- * Manages ordered broadcast delivery, applying policy to mitigate the effects of
- * slow receivers.
- */
-public class BroadcastDispatcher {
-    private static final String TAG = "BroadcastDispatcher";
-
-    // Deferred broadcasts to one app; times are all uptime time base like
-    // other broadcast-related timekeeping
-    static class Deferrals {
-        final int uid;
-        long deferredAt;    // when we started deferring
-        long deferredBy;    // how long did we defer by last time?
-        long deferUntil;    // when does the next element become deliverable?
-        int alarmCount;
-
-        final ArrayList<BroadcastRecord> broadcasts;
-
-        Deferrals(int uid, long now, long backoff, int count) {
-            this.uid = uid;
-            this.deferredAt = now;
-            this.deferredBy = backoff;
-            this.deferUntil = now + backoff;
-            this.alarmCount = count;
-            broadcasts = new ArrayList<>();
-        }
-
-        void add(BroadcastRecord br) {
-            broadcasts.add(br);
-        }
-
-        int size() {
-            return broadcasts.size();
-        }
-
-        boolean isEmpty() {
-            return broadcasts.isEmpty();
-        }
-
-        @NeverCompile
-        void dumpDebug(ProtoOutputStream proto, long fieldId) {
-            for (BroadcastRecord br : broadcasts) {
-                br.dumpDebug(proto, fieldId);
-            }
-        }
-
-        @NeverCompile
-        void dumpLocked(Dumper d) {
-            for (BroadcastRecord br : broadcasts) {
-                d.dump(br);
-            }
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("Deferrals{uid=");
-            sb.append(uid);
-            sb.append(", deferUntil=");
-            sb.append(deferUntil);
-            sb.append(", #broadcasts=");
-            sb.append(broadcasts.size());
-            sb.append("}");
-            return sb.toString();
-        }
-    }
-
-    // Carrying dump formatting state across multiple concatenated datasets
-    class Dumper {
-        final PrintWriter mPw;
-        final String mQueueName;
-        final String mDumpPackage;
-        final SimpleDateFormat mSdf;
-        boolean mPrinted;
-        boolean mNeedSep;
-        String mHeading;
-        String mLabel;
-        int mOrdinal;
-
-        Dumper(PrintWriter pw, String queueName, String dumpPackage, SimpleDateFormat sdf) {
-            mPw = pw;
-            mQueueName = queueName;
-            mDumpPackage = dumpPackage;
-            mSdf = sdf;
-
-            mPrinted = false;
-            mNeedSep = true;
-        }
-
-        void setHeading(String heading) {
-            mHeading = heading;
-            mPrinted = false;
-        }
-
-        void setLabel(String label) {
-            //"  Active Ordered Broadcast " + mQueueName + " #" + i + ":"
-            mLabel = "  " + label + " " + mQueueName + " #";
-            mOrdinal = 0;
-        }
-
-        boolean didPrint() {
-            return mPrinted;
-        }
-
-        @NeverCompile
-        void dump(BroadcastRecord br) {
-            if (mDumpPackage == null || mDumpPackage.equals(br.callerPackage)) {
-                if (!mPrinted) {
-                    if (mNeedSep) {
-                        mPw.println();
-                    }
-                    mPrinted = true;
-                    mNeedSep = true;
-                    mPw.println("  " + mHeading + " [" + mQueueName + "]:");
-                }
-                mPw.println(mLabel + mOrdinal + ":");
-                mOrdinal++;
-
-                br.dump(mPw, "    ", mSdf);
-            }
-        }
-    }
-
-    private final Object mLock;
-    private final BroadcastQueueImpl mQueue;
-    private final BroadcastConstants mConstants;
-    private final Handler mHandler;
-    private AlarmManagerInternal mAlarm;
-
-    // Current alarm targets; mapping uid -> in-flight alarm count
-    final SparseIntArray mAlarmUids = new SparseIntArray();
-    final AlarmManagerInternal.InFlightListener mAlarmListener =
-            new AlarmManagerInternal.InFlightListener() {
-        @Override
-        public void broadcastAlarmPending(final int recipientUid) {
-            synchronized (mLock) {
-                final int newCount = mAlarmUids.get(recipientUid, 0) + 1;
-                mAlarmUids.put(recipientUid, newCount);
-                // any deferred broadcasts to this app now get fast-tracked
-                final int numEntries = mDeferredBroadcasts.size();
-                for (int i = 0; i < numEntries; i++) {
-                    if (recipientUid == mDeferredBroadcasts.get(i).uid) {
-                        Deferrals d = mDeferredBroadcasts.remove(i);
-                        mAlarmDeferrals.add(d);
-                        break;
-                    }
-                }
-            }
-        }
-
-        @Override
-        public void broadcastAlarmComplete(final int recipientUid) {
-            synchronized (mLock) {
-                final int newCount = mAlarmUids.get(recipientUid, 0) - 1;
-                if (newCount >= 0) {
-                    mAlarmUids.put(recipientUid, newCount);
-                } else {
-                    Slog.wtf(TAG, "Undercount of broadcast alarms in flight for " + recipientUid);
-                    mAlarmUids.put(recipientUid, 0);
-                }
-
-                // No longer an alarm target, so resume ordinary deferral policy
-                if (newCount <= 0) {
-                    final int numEntries = mAlarmDeferrals.size();
-                    for (int i = 0; i < numEntries; i++) {
-                        if (recipientUid == mAlarmDeferrals.get(i).uid) {
-                            Deferrals d = mAlarmDeferrals.remove(i);
-                            insertLocked(mDeferredBroadcasts, d);
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    };
-
-    // Queue recheck operation used to tickle broadcast delivery when appropriate
-    final Runnable mScheduleRunnable = new Runnable() {
-        @Override
-        public void run() {
-            synchronized (mLock) {
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    Slog.v(TAG, "Deferral recheck of pending broadcasts");
-                }
-                mQueue.scheduleBroadcastsLocked();
-                mRecheckScheduled = false;
-            }
-        }
-    };
-    private boolean mRecheckScheduled = false;
-
-    // Usual issuance-order outbound queue
-    private final ArrayList<BroadcastRecord> mOrderedBroadcasts = new ArrayList<>();
-    // General deferrals not holding up alarms
-    private final ArrayList<Deferrals> mDeferredBroadcasts = new ArrayList<>();
-    // Deferrals that *are* holding up alarms; ordered by alarm dispatch time
-    private final ArrayList<Deferrals> mAlarmDeferrals = new ArrayList<>();
-    // Under the "deliver alarm broadcasts immediately" policy, the queue of
-    // upcoming alarm broadcasts.  These are always delivered first - if the
-    // policy is changed on the fly from immediate-alarm-delivery to the previous
-    // in-order-queueing behavior, pending immediate alarm deliveries will drain
-    // and then the behavior settle into the pre-U semantics.
-    private final ArrayList<BroadcastRecord> mAlarmQueue = new ArrayList<>();
-
-    // Next outbound broadcast, established by getNextBroadcastLocked()
-    private BroadcastRecord mCurrentBroadcast;
-
-    // Map userId to its deferred boot completed broadcasts.
-    private SparseArray<DeferredBootCompletedBroadcastPerUser> mUser2Deferred = new SparseArray<>();
-
-    /**
-     * Deferred LOCKED_BOOT_COMPLETED and BOOT_COMPLETED broadcasts that is sent to a user.
-     */
-    static class DeferredBootCompletedBroadcastPerUser {
-        private int mUserId;
-        // UID that has process started at least once, ready to execute LOCKED_BOOT_COMPLETED
-        // receivers.
-        @VisibleForTesting
-        SparseBooleanArray mUidReadyForLockedBootCompletedBroadcast = new SparseBooleanArray();
-        // UID that has process started at least once, ready to execute BOOT_COMPLETED receivers.
-        @VisibleForTesting
-        SparseBooleanArray mUidReadyForBootCompletedBroadcast = new SparseBooleanArray();
-        // Map UID to deferred LOCKED_BOOT_COMPLETED broadcasts.
-        // LOCKED_BOOT_COMPLETED broadcast receivers are deferred until the first time the uid has
-        // any process started.
-        @VisibleForTesting
-        SparseArray<BroadcastRecord> mDeferredLockedBootCompletedBroadcasts = new SparseArray<>();
-        // is the LOCKED_BOOT_COMPLETED broadcast received by the user.
-        @VisibleForTesting
-        boolean mLockedBootCompletedBroadcastReceived;
-        // Map UID to deferred BOOT_COMPLETED broadcasts.
-        // BOOT_COMPLETED broadcast receivers are deferred until the first time the uid has any
-        // process started.
-        @VisibleForTesting
-        SparseArray<BroadcastRecord> mDeferredBootCompletedBroadcasts = new SparseArray<>();
-        // is the BOOT_COMPLETED broadcast received by the user.
-        @VisibleForTesting
-        boolean mBootCompletedBroadcastReceived;
-
-        DeferredBootCompletedBroadcastPerUser(int userId) {
-            this.mUserId = userId;
-        }
-
-        public void updateUidReady(int uid) {
-            if (!mLockedBootCompletedBroadcastReceived
-                    || mDeferredLockedBootCompletedBroadcasts.size() != 0) {
-                mUidReadyForLockedBootCompletedBroadcast.put(uid, true);
-            }
-            if (!mBootCompletedBroadcastReceived
-                    || mDeferredBootCompletedBroadcasts.size() != 0) {
-                mUidReadyForBootCompletedBroadcast.put(uid, true);
-            }
-        }
-
-        public void enqueueBootCompletedBroadcasts(String action,
-                SparseArray<BroadcastRecord> deferred) {
-            if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
-                enqueueBootCompletedBroadcasts(deferred, mDeferredLockedBootCompletedBroadcasts,
-                        mUidReadyForLockedBootCompletedBroadcast);
-                mLockedBootCompletedBroadcastReceived = true;
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    dumpBootCompletedBroadcastRecord(mDeferredLockedBootCompletedBroadcasts);
-                }
-            } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
-                enqueueBootCompletedBroadcasts(deferred, mDeferredBootCompletedBroadcasts,
-                        mUidReadyForBootCompletedBroadcast);
-                mBootCompletedBroadcastReceived = true;
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    dumpBootCompletedBroadcastRecord(mDeferredBootCompletedBroadcasts);
-                }
-            }
-        }
-
-        /**
-         * Merge UID to BroadcastRecord map into {@link #mDeferredBootCompletedBroadcasts} or
-         * {@link #mDeferredLockedBootCompletedBroadcasts}
-         * @param from the UID to BroadcastRecord map.
-         * @param into The UID to list of BroadcastRecord map.
-         */
-        private void enqueueBootCompletedBroadcasts(SparseArray<BroadcastRecord> from,
-                SparseArray<BroadcastRecord> into, SparseBooleanArray uidReadyForReceiver) {
-            // remove unwanted uids from uidReadyForReceiver.
-            for (int i = uidReadyForReceiver.size() - 1; i >= 0; i--) {
-                if (from.indexOfKey(uidReadyForReceiver.keyAt(i)) < 0) {
-                    uidReadyForReceiver.removeAt(i);
-                }
-            }
-            for (int i = 0, size = from.size(); i < size; i++) {
-                final int uid = from.keyAt(i);
-                into.put(uid, from.valueAt(i));
-                if (uidReadyForReceiver.indexOfKey(uid) < 0) {
-                    // uid is wanted but not ready.
-                    uidReadyForReceiver.put(uid, false);
-                }
-            }
-        }
-
-        public @Nullable BroadcastRecord dequeueDeferredBootCompletedBroadcast(
-                boolean isAllUidReady) {
-            BroadcastRecord next = dequeueDeferredBootCompletedBroadcast(
-                    mDeferredLockedBootCompletedBroadcasts,
-                    mUidReadyForLockedBootCompletedBroadcast, isAllUidReady);
-            if (next == null) {
-                next = dequeueDeferredBootCompletedBroadcast(mDeferredBootCompletedBroadcasts,
-                        mUidReadyForBootCompletedBroadcast, isAllUidReady);
-            }
-            return next;
-        }
-
-        private @Nullable BroadcastRecord dequeueDeferredBootCompletedBroadcast(
-                SparseArray<BroadcastRecord> uid2br, SparseBooleanArray uidReadyForReceiver,
-                boolean isAllUidReady) {
-            for (int i = 0, size = uid2br.size(); i < size; i++) {
-                final int uid = uid2br.keyAt(i);
-                if (isAllUidReady || uidReadyForReceiver.get(uid)) {
-                    final BroadcastRecord br = uid2br.valueAt(i);
-                    if (DEBUG_BROADCAST_DEFERRAL) {
-                        final Object receiver = br.receivers.get(0);
-                        if (receiver instanceof BroadcastFilter) {
-                            if (DEBUG_BROADCAST_DEFERRAL) {
-                                Slog.i(TAG, "getDeferredBootCompletedBroadcast uid:" + uid
-                                        + " BroadcastFilter:" + (BroadcastFilter) receiver
-                                        + " broadcast:" + br.intent.getAction());
-                            }
-                        } else /* if (receiver instanceof ResolveInfo) */ {
-                            ResolveInfo info = (ResolveInfo) receiver;
-                            String packageName = info.activityInfo.applicationInfo.packageName;
-                            if (DEBUG_BROADCAST_DEFERRAL) {
-                                Slog.i(TAG, "getDeferredBootCompletedBroadcast uid:" + uid
-                                        + " packageName:" + packageName
-                                        + " broadcast:" + br.intent.getAction());
-                            }
-                        }
-                    }
-                    // remove the BroadcastRecord.
-                    uid2br.removeAt(i);
-                    if (uid2br.size() == 0) {
-                        // All deferred receivers are executed, do not need uidReadyForReceiver
-                        // any more.
-                        uidReadyForReceiver.clear();
-                    }
-                    return br;
-                }
-            }
-            return null;
-        }
-
-        private @Nullable SparseArray<BroadcastRecord> getDeferredList(String action) {
-            SparseArray<BroadcastRecord> brs = null;
-            if (action.equals(Intent.ACTION_LOCKED_BOOT_COMPLETED)) {
-                brs = mDeferredLockedBootCompletedBroadcasts;
-            } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
-                brs = mDeferredBootCompletedBroadcasts;
-            }
-            return brs;
-        }
-
-        /**
-         * Return the total number of UIDs in all BroadcastRecord in
-         * {@link #mDeferredBootCompletedBroadcasts} or
-         * {@link #mDeferredLockedBootCompletedBroadcasts}
-         */
-        private int getBootCompletedBroadcastsUidsSize(String action) {
-            SparseArray<BroadcastRecord> brs = getDeferredList(action);
-            return brs != null ? brs.size() : 0;
-        }
-
-        /**
-         * Return the total number of receivers in all BroadcastRecord in
-         * {@link #mDeferredBootCompletedBroadcasts} or
-         * {@link #mDeferredLockedBootCompletedBroadcasts}
-         */
-        private int getBootCompletedBroadcastsReceiversSize(String action) {
-            SparseArray<BroadcastRecord> brs = getDeferredList(action);
-            if (brs == null) {
-                return 0;
-            }
-            int size = 0;
-            for (int i = 0, s = brs.size(); i < s; i++) {
-                size += brs.valueAt(i).receivers.size();
-            }
-            return size;
-        }
-
-        @NeverCompile
-        public void dump(Dumper dumper, String action) {
-            SparseArray<BroadcastRecord> brs = getDeferredList(action);
-            if (brs == null) {
-                return;
-            }
-            for (int i = 0, size = brs.size(); i < size; i++) {
-                dumper.dump(brs.valueAt(i));
-            }
-        }
-
-        @NeverCompile
-        public void dumpDebug(ProtoOutputStream proto, long fieldId) {
-            for (int i = 0, size = mDeferredLockedBootCompletedBroadcasts.size(); i < size; i++) {
-                mDeferredLockedBootCompletedBroadcasts.valueAt(i).dumpDebug(proto, fieldId);
-            }
-            for (int i = 0, size = mDeferredBootCompletedBroadcasts.size(); i < size; i++) {
-                mDeferredBootCompletedBroadcasts.valueAt(i).dumpDebug(proto, fieldId);
-            }
-        }
-
-        @NeverCompile
-        private void dumpBootCompletedBroadcastRecord(SparseArray<BroadcastRecord> brs) {
-            for (int i = 0, size = brs.size(); i < size; i++) {
-                final Object receiver = brs.valueAt(i).receivers.get(0);
-                String packageName = null;
-                if (receiver instanceof BroadcastFilter) {
-                    BroadcastFilter recv = (BroadcastFilter) receiver;
-                    packageName = recv.receiverList.app.processName;
-                } else /* if (receiver instanceof ResolveInfo) */ {
-                    ResolveInfo info = (ResolveInfo) receiver;
-                    packageName = info.activityInfo.applicationInfo.packageName;
-                }
-                Slog.i(TAG, "uid:" + brs.keyAt(i)
-                        + " packageName:" + packageName
-                        + " receivers:" + brs.valueAt(i).receivers.size());
-            }
-        }
-    }
-
-    private DeferredBootCompletedBroadcastPerUser getDeferredPerUser(int userId) {
-        if (mUser2Deferred.contains(userId)) {
-            return mUser2Deferred.get(userId);
-        } else {
-            final DeferredBootCompletedBroadcastPerUser temp =
-                    new DeferredBootCompletedBroadcastPerUser(userId);
-            mUser2Deferred.put(userId, temp);
-            return temp;
-        }
-    }
-
-    /**
-     * ActivityManagerService.attachApplication() call this method to notify that the UID is ready
-     * to accept deferred LOCKED_BOOT_COMPLETED and BOOT_COMPLETED broadcasts.
-     * @param uid
-     */
-    public void updateUidReadyForBootCompletedBroadcastLocked(int uid) {
-        getDeferredPerUser(UserHandle.getUserId(uid)).updateUidReady(uid);
-    }
-
-    private @Nullable BroadcastRecord dequeueDeferredBootCompletedBroadcast() {
-        final boolean isAllUidReady = (mQueue.mService.mConstants.mDeferBootCompletedBroadcast
-                == DEFER_BOOT_COMPLETED_BROADCAST_NONE);
-        BroadcastRecord next = null;
-        for (int i = 0, size = mUser2Deferred.size(); i < size; i++) {
-            next = mUser2Deferred.valueAt(i).dequeueDeferredBootCompletedBroadcast(isAllUidReady);
-            if (next != null) {
-                break;
-            }
-        }
-        return next;
-    }
-
-    /**
-     * Constructed & sharing a lock with its associated BroadcastQueue instance
-     */
-    public BroadcastDispatcher(BroadcastQueueImpl queue, BroadcastConstants constants,
-            Handler handler, Object lock) {
-        mQueue = queue;
-        mConstants = constants;
-        mHandler = handler;
-        mLock = lock;
-    }
-
-    /**
-     * Spin up the integration with the alarm manager service; done lazily to manage
-     * service availability ordering during boot.
-     */
-    public void start() {
-        // Set up broadcast alarm tracking
-        mAlarm = LocalServices.getService(AlarmManagerInternal.class);
-        mAlarm.registerInFlightListener(mAlarmListener);
-    }
-
-    /**
-     * Standard contents-are-empty check
-     */
-    public boolean isEmpty() {
-        synchronized (mLock) {
-            return isIdle()
-                    && getBootCompletedBroadcastsUidsSize(Intent.ACTION_LOCKED_BOOT_COMPLETED) == 0
-                    && getBootCompletedBroadcastsUidsSize(Intent.ACTION_BOOT_COMPLETED) == 0;
-        }
-    }
-
-    /**
-     * Have less check than {@link #isEmpty()}.
-     * The dispatcher is considered as idle even with deferred LOCKED_BOOT_COMPLETED/BOOT_COMPLETED
-     * broadcasts because those can be deferred until the first time the uid's process is started.
-     * @return
-     */
-    public boolean isIdle() {
-        synchronized (mLock) {
-            return mCurrentBroadcast == null
-                    && mOrderedBroadcasts.isEmpty()
-                    && mAlarmQueue.isEmpty()
-                    && isDeferralsListEmpty(mDeferredBroadcasts)
-                    && isDeferralsListEmpty(mAlarmDeferrals);
-        }
-    }
-
-    private static boolean isDeferralsBeyondBarrier(@NonNull ArrayList<Deferrals> list,
-            @UptimeMillisLong long barrierTime) {
-        for (int i = 0; i < list.size(); i++) {
-            if (!isBeyondBarrier(list.get(i).broadcasts, barrierTime)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static boolean isBeyondBarrier(@NonNull ArrayList<BroadcastRecord> list,
-            @UptimeMillisLong long barrierTime) {
-        for (int i = 0; i < list.size(); i++) {
-            if (list.get(i).enqueueTime <= barrierTime) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public boolean isBeyondBarrier(@UptimeMillisLong long barrierTime) {
-        synchronized (mLock) {
-            if ((mCurrentBroadcast != null) && mCurrentBroadcast.enqueueTime <= barrierTime) {
-                return false;
-            }
-            return isBeyondBarrier(mOrderedBroadcasts, barrierTime)
-                    && isBeyondBarrier(mAlarmQueue, barrierTime)
-                    && isDeferralsBeyondBarrier(mDeferredBroadcasts, barrierTime)
-                    && isDeferralsBeyondBarrier(mAlarmDeferrals, barrierTime);
-        }
-    }
-
-    private static boolean isDispatchedInDeferrals(@NonNull ArrayList<Deferrals> list,
-            @NonNull Intent intent) {
-        for (int i = 0; i < list.size(); i++) {
-            if (!isDispatched(list.get(i).broadcasts, intent)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static boolean isDispatched(@NonNull ArrayList<BroadcastRecord> list,
-            @NonNull Intent intent) {
-        for (int i = 0; i < list.size(); i++) {
-            if (intent.filterEquals(list.get(i).intent)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public boolean isDispatched(@NonNull Intent intent) {
-        synchronized (mLock) {
-            if ((mCurrentBroadcast != null) && intent.filterEquals(mCurrentBroadcast.intent)) {
-                return false;
-            }
-            return isDispatched(mOrderedBroadcasts, intent)
-                    && isDispatched(mAlarmQueue, intent)
-                    && isDispatchedInDeferrals(mDeferredBroadcasts, intent)
-                    && isDispatchedInDeferrals(mAlarmDeferrals, intent);
-        }
-    }
-
-    private static int pendingInDeferralsList(ArrayList<Deferrals> list) {
-        int pending = 0;
-        final int numEntries = list.size();
-        for (int i = 0; i < numEntries; i++) {
-            pending += list.get(i).size();
-        }
-        return pending;
-    }
-
-    private static boolean isDeferralsListEmpty(ArrayList<Deferrals> list) {
-        return pendingInDeferralsList(list) == 0;
-    }
-
-    /**
-     * Strictly for logging, describe the currently pending contents in a human-
-     * readable way
-     */
-    public String describeStateLocked() {
-        final StringBuilder sb = new StringBuilder(128);
-        if (mCurrentBroadcast != null) {
-            sb.append("1 in flight, ");
-        }
-        sb.append(mOrderedBroadcasts.size());
-        sb.append(" ordered");
-        int n = mAlarmQueue.size();
-        if (n > 0) {
-            sb.append(", ");
-            sb.append(n);
-            sb.append(" alarms");
-        }
-        n = pendingInDeferralsList(mAlarmDeferrals);
-        if (n > 0) {
-            sb.append(", ");
-            sb.append(n);
-            sb.append(" deferrals in alarm recipients");
-        }
-        n = pendingInDeferralsList(mDeferredBroadcasts);
-        if (n > 0) {
-            sb.append(", ");
-            sb.append(n);
-            sb.append(" deferred");
-        }
-        n = getBootCompletedBroadcastsUidsSize(Intent.ACTION_LOCKED_BOOT_COMPLETED);
-        if (n > 0) {
-            sb.append(", ");
-            sb.append(n);
-            sb.append(" deferred LOCKED_BOOT_COMPLETED/");
-            sb.append(getBootCompletedBroadcastsReceiversSize(Intent.ACTION_LOCKED_BOOT_COMPLETED));
-            sb.append(" receivers");
-        }
-
-        n = getBootCompletedBroadcastsUidsSize(Intent.ACTION_BOOT_COMPLETED);
-        if (n > 0) {
-            sb.append(", ");
-            sb.append(n);
-            sb.append(" deferred BOOT_COMPLETED/");
-            sb.append(getBootCompletedBroadcastsReceiversSize(Intent.ACTION_BOOT_COMPLETED));
-            sb.append(" receivers");
-        }
-        return sb.toString();
-    }
-
-    // ----------------------------------
-    // BroadcastQueue operation support
-    void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
-        final ArrayList<BroadcastRecord> queue =
-                (r.alarm && mQueue.mService.mConstants.mPrioritizeAlarmBroadcasts)
-                        ? mAlarmQueue
-                        : mOrderedBroadcasts;
-
-        if (r.receivers == null || r.receivers.isEmpty()) {
-            // Fast no-op path for broadcasts that won't actually be dispatched to
-            // receivers - we still need to handle completion callbacks and historical
-            // records, but we don't need to consider the fancy cases.
-            queue.add(r);
-            return;
-        }
-
-        if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(r.intent.getAction())) {
-            // Create one BroadcastRecord for each UID that can be deferred.
-            final SparseArray<BroadcastRecord> deferred =
-                    r.splitDeferredBootCompletedBroadcastLocked(mQueue.mService.mInternal,
-                            mQueue.mService.mConstants.mDeferBootCompletedBroadcast);
-            getDeferredPerUser(r.userId).enqueueBootCompletedBroadcasts(
-                    Intent.ACTION_LOCKED_BOOT_COMPLETED, deferred);
-            if (!r.receivers.isEmpty()) {
-                // The non-deferred receivers.
-                mOrderedBroadcasts.add(r);
-                return;
-            }
-        } else if (Intent.ACTION_BOOT_COMPLETED.equals(r.intent.getAction())) {
-            // Create one BroadcastRecord for each UID that can be deferred.
-            final SparseArray<BroadcastRecord> deferred =
-                    r.splitDeferredBootCompletedBroadcastLocked(mQueue.mService.mInternal,
-                            mQueue.mService.mConstants.mDeferBootCompletedBroadcast);
-            getDeferredPerUser(r.userId).enqueueBootCompletedBroadcasts(
-                    Intent.ACTION_BOOT_COMPLETED, deferred);
-            if (!r.receivers.isEmpty()) {
-                // The non-deferred receivers.
-                mOrderedBroadcasts.add(r);
-                return;
-            }
-        } else {
-            // Ordinary broadcast, so put it on the appropriate queue and carry on
-            queue.add(r);
-        }
-    }
-
-    /**
-     * Return the total number of UIDs in all deferred boot completed BroadcastRecord.
-     */
-    private int getBootCompletedBroadcastsUidsSize(String action) {
-        int size = 0;
-        for (int i = 0, s = mUser2Deferred.size(); i < s; i++) {
-            size += mUser2Deferred.valueAt(i).getBootCompletedBroadcastsUidsSize(action);
-        }
-        return size;
-    }
-
-    /**
-     * Return the total number of receivers in all deferred boot completed BroadcastRecord.
-     */
-    private int getBootCompletedBroadcastsReceiversSize(String action) {
-        int size = 0;
-        for (int i = 0, s = mUser2Deferred.size(); i < s; i++) {
-            size += mUser2Deferred.valueAt(i).getBootCompletedBroadcastsReceiversSize(action);
-        }
-        return size;
-    }
-
-    // Returns the now-replaced broadcast record, or null if none
-    BroadcastRecord replaceBroadcastLocked(BroadcastRecord r, String typeForLogging) {
-        // Simple case, in the ordinary queue.
-        BroadcastRecord old = replaceBroadcastLocked(mOrderedBroadcasts, r, typeForLogging);
-        // ... or possibly in the simple alarm queue
-        if (old == null) {
-            old = replaceBroadcastLocked(mAlarmQueue, r, typeForLogging);
-        }
-        // If we didn't find it, less-simple:  in a deferral queue?
-        if (old == null) {
-            old = replaceDeferredBroadcastLocked(mAlarmDeferrals, r, typeForLogging);
-        }
-        if (old == null) {
-            old = replaceDeferredBroadcastLocked(mDeferredBroadcasts, r, typeForLogging);
-        }
-        return old;
-    }
-
-    private BroadcastRecord replaceDeferredBroadcastLocked(ArrayList<Deferrals> list,
-            BroadcastRecord r, String typeForLogging) {
-        BroadcastRecord old;
-        final int numEntries = list.size();
-        for (int i = 0; i < numEntries; i++) {
-            final Deferrals d = list.get(i);
-            old = replaceBroadcastLocked(d.broadcasts, r, typeForLogging);
-            if (old != null) {
-                return old;
-            }
-        }
-        return null;
-    }
-
-    private BroadcastRecord replaceBroadcastLocked(ArrayList<BroadcastRecord> list,
-            BroadcastRecord r, String typeForLogging) {
-        BroadcastRecord old;
-        final Intent intent = r.intent;
-        // Any in-flight broadcast has already been popped, and cannot be replaced.
-        // (This preserves existing behavior of the replacement API)
-        for (int i = list.size() - 1; i >= 0; i--) {
-            old = list.get(i);
-            if (old.userId == r.userId && intent.filterEquals(old.intent)) {
-                if (DEBUG_BROADCAST) {
-                    Slog.v(TAG, "***** Replacing " + typeForLogging
-                            + " [" + mQueue.mQueueName + "]: " + intent);
-                }
-                // Clone deferral state too if any
-                r.deferred = old.deferred;
-                list.set(i, r);
-                return old;
-            }
-        }
-        return null;
-    }
-
-    boolean cleanupDisabledPackageReceiversLocked(final String packageName,
-            Set<String> filterByClasses, final int userId, final boolean doit) {
-        // Note: fast short circuits when 'doit' is false, as soon as we hit any
-        // "yes we would do something" circumstance
-        boolean didSomething = cleanupBroadcastListDisabledReceiversLocked(mOrderedBroadcasts,
-                packageName, filterByClasses, userId, doit);
-        if (doit || !didSomething) {
-            didSomething = cleanupBroadcastListDisabledReceiversLocked(mAlarmQueue,
-                    packageName, filterByClasses, userId, doit);
-        }
-        if (doit || !didSomething) {
-            ArrayList<BroadcastRecord> lockedBootCompletedBroadcasts = new ArrayList<>();
-            for (int u = 0, usize = mUser2Deferred.size(); u < usize; u++) {
-                SparseArray<BroadcastRecord> brs =
-                        mUser2Deferred.valueAt(u).mDeferredLockedBootCompletedBroadcasts;
-                for (int i = 0, size = brs.size(); i < size; i++) {
-                    lockedBootCompletedBroadcasts.add(brs.valueAt(i));
-                }
-            }
-            didSomething = cleanupBroadcastListDisabledReceiversLocked(
-                    lockedBootCompletedBroadcasts,
-                    packageName, filterByClasses, userId, doit);
-        }
-        if (doit || !didSomething) {
-            ArrayList<BroadcastRecord> bootCompletedBroadcasts = new ArrayList<>();
-            for (int u = 0, usize = mUser2Deferred.size(); u < usize; u++) {
-                SparseArray<BroadcastRecord> brs =
-                        mUser2Deferred.valueAt(u).mDeferredBootCompletedBroadcasts;
-                for (int i = 0, size = brs.size(); i < size; i++) {
-                    bootCompletedBroadcasts.add(brs.valueAt(i));
-                }
-            }
-            didSomething = cleanupBroadcastListDisabledReceiversLocked(bootCompletedBroadcasts,
-                    packageName, filterByClasses, userId, doit);
-        }
-        if (doit || !didSomething) {
-            didSomething |= cleanupDeferralsListDisabledReceiversLocked(mAlarmDeferrals,
-                    packageName, filterByClasses, userId, doit);
-        }
-        if (doit || !didSomething) {
-            didSomething |= cleanupDeferralsListDisabledReceiversLocked(mDeferredBroadcasts,
-                    packageName, filterByClasses, userId, doit);
-        }
-        if ((doit || !didSomething) && mCurrentBroadcast != null) {
-            didSomething |= mCurrentBroadcast.cleanupDisabledPackageReceiversLocked(
-                    packageName, filterByClasses, userId, doit);
-        }
-
-        return didSomething;
-    }
-
-    private boolean cleanupDeferralsListDisabledReceiversLocked(ArrayList<Deferrals> list,
-            final String packageName, Set<String> filterByClasses, final int userId,
-            final boolean doit) {
-        boolean didSomething = false;
-        for (Deferrals d : list) {
-            didSomething = cleanupBroadcastListDisabledReceiversLocked(d.broadcasts,
-                    packageName, filterByClasses, userId, doit);
-            if (!doit && didSomething) {
-                return true;
-            }
-        }
-        return didSomething;
-    }
-
-    private boolean cleanupBroadcastListDisabledReceiversLocked(ArrayList<BroadcastRecord> list,
-            final String packageName, Set<String> filterByClasses, final int userId,
-            final boolean doit) {
-        boolean didSomething = false;
-        for (BroadcastRecord br : list) {
-            didSomething |= br.cleanupDisabledPackageReceiversLocked(packageName,
-                    filterByClasses, userId, doit);
-            if (!doit && didSomething) {
-                return true;
-            }
-        }
-        return didSomething;
-    }
-
-    /**
-     * Standard proto dump entry point
-     */
-    @NeverCompile
-    public void dumpDebug(ProtoOutputStream proto, long fieldId) {
-        if (mCurrentBroadcast != null) {
-            mCurrentBroadcast.dumpDebug(proto, fieldId);
-        }
-        for (Deferrals d : mAlarmDeferrals) {
-            d.dumpDebug(proto, fieldId);
-        }
-        for (BroadcastRecord br : mOrderedBroadcasts) {
-            br.dumpDebug(proto, fieldId);
-        }
-        for (BroadcastRecord br : mAlarmQueue) {
-            br.dumpDebug(proto, fieldId);
-        }
-        for (Deferrals d : mDeferredBroadcasts) {
-            d.dumpDebug(proto, fieldId);
-        }
-
-        for (int i = 0, size = mUser2Deferred.size(); i < size; i++) {
-            mUser2Deferred.valueAt(i).dumpDebug(proto, fieldId);
-        }
-    }
-
-    // ----------------------------------
-    // Dispatch & deferral management
-
-    public BroadcastRecord getActiveBroadcastLocked() {
-        return mCurrentBroadcast;
-    }
-
-    /**
-     * If there is a deferred broadcast that is being sent to an alarm target, return
-     * that one.  If there's no deferred alarm target broadcast but there is one
-     * that has reached the end of its deferral, return that.
-     *
-     * This stages the broadcast internally until it is retired, and returns that
-     * staged record if this is called repeatedly, until retireBroadcast(r) is called.
-     */
-    public BroadcastRecord getNextBroadcastLocked(final long now) {
-        if (mCurrentBroadcast != null) {
-            return mCurrentBroadcast;
-        }
-
-        BroadcastRecord next = null;
-
-        // Alarms in flight take precedence over everything else.  This queue
-        // will be non-empty only when the relevant policy is in force, but if
-        // policy has changed on the fly we still need to drain this before we
-        // settle into the legacy behavior.
-        if (!mAlarmQueue.isEmpty()) {
-            next = mAlarmQueue.remove(0);
-        }
-
-        // Next in precedence are deferred BOOT_COMPLETED broadcasts
-        if (next == null) {
-            next = dequeueDeferredBootCompletedBroadcast();
-        }
-
-        // Alarm-related deferrals are next in precedence...
-        if (next == null && !mAlarmDeferrals.isEmpty()) {
-            next = popLocked(mAlarmDeferrals);
-            if (DEBUG_BROADCAST_DEFERRAL && next != null) {
-                Slog.i(TAG, "Next broadcast from alarm targets: " + next);
-            }
-        }
-
-        final boolean someQueued = !mOrderedBroadcasts.isEmpty();
-
-        if (next == null && !mDeferredBroadcasts.isEmpty()) {
-            // A this point we're going to deliver either:
-            // 1. the next "overdue" deferral; or
-            // 2. the next ordinary ordered broadcast; *or*
-            // 3. the next not-yet-overdue deferral.
-
-            for (int i = 0; i < mDeferredBroadcasts.size(); i++) {
-                Deferrals d = mDeferredBroadcasts.get(i);
-                if (now < d.deferUntil && someQueued) {
-                    // stop looking when we haven't hit the next time-out boundary
-                    // but only if we have un-deferred broadcasts waiting,
-                    // otherwise we can deliver whatever deferred broadcast
-                    // is next available.
-                    break;
-                }
-
-                if (d.broadcasts.size() > 0) {
-                    next = d.broadcasts.remove(0);
-                    // apply deferral-interval decay policy and move this uid's
-                    // deferred broadcasts down in the delivery queue accordingly
-                    mDeferredBroadcasts.remove(i); // already 'd'
-                    d.deferredBy = calculateDeferral(d.deferredBy);
-                    d.deferUntil += d.deferredBy;
-                    insertLocked(mDeferredBroadcasts, d);
-                    if (DEBUG_BROADCAST_DEFERRAL) {
-                        Slog.i(TAG, "Next broadcast from deferrals " + next
-                                + ", deferUntil now " + d.deferUntil);
-                    }
-                    break;
-                }
-            }
-        }
-
-        if (next == null && someQueued) {
-            next = mOrderedBroadcasts.remove(0);
-            if (DEBUG_BROADCAST_DEFERRAL) {
-                Slog.i(TAG, "Next broadcast from main queue: " + next);
-            }
-        }
-
-        mCurrentBroadcast = next;
-        return next;
-    }
-
-    /**
-     * Called after the broadcast queue finishes processing the currently
-     * active broadcast (obtained by calling getNextBroadcastLocked()).
-     */
-    public void retireBroadcastLocked(final BroadcastRecord r) {
-        // ERROR if 'r' is not the active broadcast
-        if (r != mCurrentBroadcast) {
-            Slog.wtf(TAG, "Retiring broadcast " + r
-                    + " doesn't match current outgoing " + mCurrentBroadcast);
-        }
-        mCurrentBroadcast = null;
-    }
-
-    /**
-     * Called prior to broadcast dispatch to check whether the intended
-     * recipient is currently subject to deferral policy.
-     */
-    public boolean isDeferringLocked(final int uid) {
-        Deferrals d = findUidLocked(uid);
-        if (d != null && d.broadcasts.isEmpty()) {
-            // once we've caught up with deferred broadcasts to this uid
-            // and time has advanced sufficiently that we wouldn't be
-            // deferring newly-enqueued ones, we're back to normal policy.
-            if (SystemClock.uptimeMillis() >= d.deferUntil) {
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    Slog.i(TAG, "No longer deferring broadcasts to uid " + d.uid);
-                }
-                removeDeferral(d);
-                return false;
-            }
-        }
-        return (d != null);
-    }
-
-    /**
-     * Defer broadcasts for the given app.  If 'br' is non-null, this also makes
-     * sure that broadcast record is enqueued as the next upcoming broadcast for
-     * the app.
-     */
-    public void startDeferring(final int uid) {
-        synchronized (mLock) {
-            Deferrals d = findUidLocked(uid);
-
-            // If we're not yet tracking this app, set up that bookkeeping
-            if (d == null) {
-                // Start a new deferral
-                final long now = SystemClock.uptimeMillis();
-                d = new Deferrals(uid,
-                        now,
-                        mConstants.DEFERRAL,
-                        mAlarmUids.get(uid, 0));
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    Slog.i(TAG, "Now deferring broadcasts to " + uid
-                            + " until " + d.deferUntil);
-                }
-                // where it goes depends on whether it is coming into an alarm-related situation
-                if (d.alarmCount == 0) {
-                    // common case, put it in the ordinary priority queue
-                    insertLocked(mDeferredBroadcasts, d);
-                    scheduleDeferralCheckLocked(true);
-                } else {
-                    // alarm-related: strict order-encountered
-                    mAlarmDeferrals.add(d);
-                }
-            } else {
-                // We're already deferring, but something was slow again.  Reset the
-                // deferral decay progression.
-                d.deferredBy = mConstants.DEFERRAL;
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    Slog.i(TAG, "Uid " + uid + " slow again, deferral interval reset to "
-                            + d.deferredBy);
-                }
-            }
-        }
-    }
-
-    /**
-     * Key entry point when a broadcast about to be delivered is instead
-     * set aside for deferred delivery
-     */
-    public void addDeferredBroadcast(final int uid, BroadcastRecord br) {
-        if (DEBUG_BROADCAST_DEFERRAL) {
-            Slog.i(TAG, "Enqueuing deferred broadcast " + br);
-        }
-        synchronized (mLock) {
-            Deferrals d = findUidLocked(uid);
-            if (d == null) {
-                Slog.wtf(TAG, "Adding deferred broadcast but not tracking " + uid);
-            } else {
-                if (br == null) {
-                    Slog.wtf(TAG, "Deferring null broadcast to " + uid);
-                } else {
-                    br.deferred = true;
-                    d.add(br);
-                }
-            }
-        }
-    }
-
-    /**
-     * When there are deferred broadcasts, we need to make sure to recheck the
-     * dispatch queue when they come due.  Alarm-sensitive deferrals get dispatched
-     * aggressively, so we only need to use the ordinary deferrals timing to figure
-     * out when to recheck.
-     */
-    public void scheduleDeferralCheckLocked(boolean force) {
-        if ((force || !mRecheckScheduled) && !mDeferredBroadcasts.isEmpty()) {
-            final Deferrals d = mDeferredBroadcasts.get(0);
-            if (!d.broadcasts.isEmpty()) {
-                mHandler.removeCallbacks(mScheduleRunnable);
-                mHandler.postAtTime(mScheduleRunnable, d.deferUntil);
-                mRecheckScheduled = true;
-                if (DEBUG_BROADCAST_DEFERRAL) {
-                    Slog.i(TAG, "Scheduling deferred broadcast recheck at " + d.deferUntil);
-                }
-            }
-        }
-    }
-
-    /**
-     * Cancel all current deferrals; that is, make all currently-deferred broadcasts
-     * immediately deliverable.  Used by the wait-for-broadcast-idle mechanism.
-     */
-    public void cancelDeferralsLocked() {
-        zeroDeferralTimes(mAlarmDeferrals);
-        zeroDeferralTimes(mDeferredBroadcasts);
-    }
-
-    private static void zeroDeferralTimes(ArrayList<Deferrals> list) {
-        final int num = list.size();
-        for (int i = 0; i < num; i++) {
-            Deferrals d = list.get(i);
-            // Safe to do this in-place because it won't break ordering
-            d.deferUntil = d.deferredBy = 0;
-        }
-    }
-
-    // ----------------------------------
-
-    /**
-     * If broadcasts to this uid are being deferred, find the deferrals record about it.
-     * @return null if this uid's broadcasts are not being deferred
-     */
-    private Deferrals findUidLocked(final int uid) {
-        // The common case is that they it isn't also an alarm target...
-        Deferrals d = findUidLocked(uid, mDeferredBroadcasts);
-        // ...but if not there, also check alarm-prioritized deferrals
-        if (d == null) {
-            d = findUidLocked(uid, mAlarmDeferrals);
-        }
-        return d;
-    }
-
-    /**
-     * Remove the given deferral record from whichever queue it might be in at present
-     * @return true if the deferral was in fact found, false if this made no changes
-     */
-    private boolean removeDeferral(Deferrals d) {
-        boolean didRemove = mDeferredBroadcasts.remove(d);
-        if (!didRemove) {
-            didRemove = mAlarmDeferrals.remove(d);
-        }
-        return didRemove;
-    }
-
-    /**
-     * Find the deferrals record for the given uid in the given list
-     */
-    private static Deferrals findUidLocked(final int uid, ArrayList<Deferrals> list) {
-        final int numElements = list.size();
-        for (int i = 0; i < numElements; i++) {
-            Deferrals d = list.get(i);
-            if (uid == d.uid) {
-                return d;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Pop the next broadcast record from the head of the given deferrals list,
-     * if one exists.
-     */
-    private static BroadcastRecord popLocked(ArrayList<Deferrals> list) {
-        final Deferrals d = list.get(0);
-        return d.broadcasts.isEmpty() ? null : d.broadcasts.remove(0);
-    }
-
-    /**
-     * Insert the given Deferrals into the priority queue, sorted by defer-until milestone
-     */
-    private static void insertLocked(ArrayList<Deferrals> list, Deferrals d) {
-        // Simple linear search is appropriate here because we expect to
-        // have very few entries in the deferral lists (i.e. very few badly-
-        // behaving apps currently facing deferral)
-        int i;
-        final int numElements = list.size();
-        for (i = 0; i < numElements; i++) {
-            if (d.deferUntil < list.get(i).deferUntil) {
-                break;
-            }
-        }
-        list.add(i, d);
-    }
-
-    /**
-     * Calculate a new deferral time based on the previous time.  This should decay
-     * toward zero, though a small nonzero floor is an option.
-     */
-    private long calculateDeferral(long previous) {
-        return Math.max(mConstants.DEFERRAL_FLOOR,
-                (long) (previous * mConstants.DEFERRAL_DECAY_FACTOR));
-    }
-
-    // ----------------------------------
-
-    @NeverCompile
-    boolean dumpLocked(PrintWriter pw, String dumpPackage, String queueName,
-            SimpleDateFormat sdf) {
-        final Dumper dumper = new Dumper(pw, queueName, dumpPackage, sdf);
-        boolean printed = false;
-
-        dumper.setHeading("Currently in flight");
-        dumper.setLabel("In-Flight Ordered Broadcast");
-        if (mCurrentBroadcast != null) {
-            dumper.dump(mCurrentBroadcast);
-        } else {
-            pw.println("  (null)");
-        }
-        printed |= dumper.didPrint();
-
-        dumper.setHeading("Active alarm broadcasts");
-        dumper.setLabel("Active Alarm Broadcast");
-        for (BroadcastRecord br : mAlarmQueue) {
-            dumper.dump(br);
-        }
-        printed |= dumper.didPrint();
-
-        dumper.setHeading("Active ordered broadcasts");
-        dumper.setLabel("Active Ordered Broadcast");
-        for (Deferrals d : mAlarmDeferrals) {
-            d.dumpLocked(dumper);
-        }
-        for (BroadcastRecord br : mOrderedBroadcasts) {
-            dumper.dump(br);
-        }
-        printed |= dumper.didPrint();
-
-        dumper.setHeading("Deferred ordered broadcasts");
-        dumper.setLabel("Deferred Ordered Broadcast");
-        for (Deferrals d : mDeferredBroadcasts) {
-            d.dumpLocked(dumper);
-        }
-        printed |= dumper.didPrint();
-
-        dumper.setHeading("Deferred LOCKED_BOOT_COMPLETED broadcasts");
-        dumper.setLabel("Deferred LOCKED_BOOT_COMPLETED Broadcast");
-        for (int i = 0, size = mUser2Deferred.size(); i < size; i++) {
-            mUser2Deferred.valueAt(i).dump(dumper, Intent.ACTION_LOCKED_BOOT_COMPLETED);
-        }
-        printed |= dumper.didPrint();
-
-        dumper.setHeading("Deferred BOOT_COMPLETED broadcasts");
-        dumper.setLabel("Deferred BOOT_COMPLETED Broadcast");
-        for (int i = 0, size = mUser2Deferred.size(); i < size; i++) {
-            mUser2Deferred.valueAt(i).dump(dumper, Intent.ACTION_BOOT_COMPLETED);
-        }
-        printed |= dumper.didPrint();
-
-        return printed;
-    }
-}
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index d1c8c30..4a37913 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -276,6 +276,11 @@
                 && record.getDeliveryGroupPolicy() == BroadcastOptions.DELIVERY_GROUP_POLICY_ALL) {
             final BroadcastRecord replacedBroadcastRecord = replaceBroadcast(record, recordIndex);
             if (replacedBroadcastRecord != null) {
+                if (mLastDeferredStates && shouldBeDeferred()
+                        && (record.getDeliveryState(recordIndex)
+                                == BroadcastRecord.DELIVERY_PENDING)) {
+                    deferredStatesApplyConsumer.accept(record, recordIndex);
+                }
                 return replacedBroadcastRecord;
             }
         }
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
deleted file mode 100644
index 3c56752..0000000
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ /dev/null
@@ -1,1983 +0,0 @@
-/*
- * Copyright (C) 2012 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.am;
-
-import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET;
-import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER;
-import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED;
-import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
-import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
-import static android.text.TextUtils.formatSimple;
-
-import static com.android.internal.util.FrameworkStatsLog.BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED;
-import static com.android.internal.util.FrameworkStatsLog.BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__BOOT_COMPLETED;
-import static com.android.internal.util.FrameworkStatsLog.BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__LOCKED_BOOT_COMPLETED;
-import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED;
-import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
-import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM;
-import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST;
-import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME;
-import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
-import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_DEFERRAL;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_BROADCAST;
-import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.ApplicationExitInfo;
-import android.app.BroadcastOptions;
-import android.app.IApplicationThread;
-import android.app.usage.UsageEvents.Event;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.IIntentReceiver;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.UserInfo;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerExemptionManager.ReasonCode;
-import android.os.PowerExemptionManager.TempAllowListType;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.EventLog;
-import android.util.IndentingPrintWriter;
-import android.util.Slog;
-import android.util.SparseIntArray;
-import android.util.proto.ProtoOutputStream;
-
-import com.android.internal.os.TimeoutRecord;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.server.LocalServices;
-import com.android.server.pm.UserJourneyLogger;
-import com.android.server.pm.UserManagerInternal;
-
-import dalvik.annotation.optimization.NeverCompile;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Set;
-import java.util.function.BooleanSupplier;
-
-/**
- * BROADCASTS
- *
- * We keep three broadcast queues and associated bookkeeping, one for those at
- * foreground priority, and one for normal (background-priority) broadcasts, and one to
- * offload special broadcasts that we know take a long time, such as BOOT_COMPLETED.
- */
-public class BroadcastQueueImpl extends BroadcastQueue {
-    private static final String TAG_MU = TAG + POSTFIX_MU;
-    private static final String TAG_BROADCAST = TAG + POSTFIX_BROADCAST;
-
-    final BroadcastConstants mConstants;
-
-    /**
-     * If true, we can delay broadcasts while waiting services to finish in the previous
-     * receiver's process.
-     */
-    final boolean mDelayBehindServices;
-
-    final int mSchedGroup;
-
-    /**
-     * Lists of all active broadcasts that are to be executed immediately
-     * (without waiting for another broadcast to finish).  Currently this only
-     * contains broadcasts to registered receivers, to avoid spinning up
-     * a bunch of processes to execute IntentReceiver components.  Background-
-     * and foreground-priority broadcasts are queued separately.
-     */
-    final ArrayList<BroadcastRecord> mParallelBroadcasts = new ArrayList<>();
-
-    /**
-     * Tracking of the ordered broadcast queue, including deferral policy and alarm
-     * prioritization.
-     */
-    final BroadcastDispatcher mDispatcher;
-
-    /**
-     * Refcounting for completion callbacks of split/deferred broadcasts.  The key
-     * is an opaque integer token assigned lazily when a broadcast is first split
-     * into multiple BroadcastRecord objects.
-     */
-    final SparseIntArray mSplitRefcounts = new SparseIntArray();
-    private int mNextToken = 0;
-
-    /**
-     * Set when we current have a BROADCAST_INTENT_MSG in flight.
-     */
-    boolean mBroadcastsScheduled = false;
-
-    /**
-     * True if we have a pending unexpired BROADCAST_TIMEOUT_MSG posted to our handler.
-     */
-    boolean mPendingBroadcastTimeoutMessage;
-
-    /**
-     * Intent broadcasts that we have tried to start, but are
-     * waiting for the application's process to be created.  We only
-     * need one per scheduling class (instead of a list) because we always
-     * process broadcasts one at a time, so no others can be started while
-     * waiting for this one.
-     */
-    BroadcastRecord mPendingBroadcast = null;
-
-    /**
-     * The receiver index that is pending, to restart the broadcast if needed.
-     */
-    int mPendingBroadcastRecvIndex;
-
-    static final int BROADCAST_INTENT_MSG = ActivityManagerService.FIRST_BROADCAST_QUEUE_MSG;
-    static final int BROADCAST_TIMEOUT_MSG = ActivityManagerService.FIRST_BROADCAST_QUEUE_MSG + 1;
-
-    // log latency metrics for ordered broadcasts during BOOT_COMPLETED processing
-    boolean mLogLatencyMetrics = true;
-
-    final BroadcastHandler mHandler;
-
-    private final class BroadcastHandler extends Handler {
-        public BroadcastHandler(Looper looper) {
-            super(looper, null);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case BROADCAST_INTENT_MSG: {
-                    if (DEBUG_BROADCAST) Slog.v(
-                            TAG_BROADCAST, "Received BROADCAST_INTENT_MSG ["
-                            + mQueueName + "]");
-                    processNextBroadcast(true);
-                } break;
-                case BROADCAST_TIMEOUT_MSG: {
-                    synchronized (mService) {
-                        broadcastTimeoutLocked(true);
-                    }
-                } break;
-            }
-        }
-    }
-
-    BroadcastQueueImpl(ActivityManagerService service, Handler handler,
-            String name, BroadcastConstants constants, boolean allowDelayBehindServices,
-            int schedGroup) {
-        this(service, handler, name, constants, new BroadcastSkipPolicy(service),
-                new BroadcastHistory(constants), allowDelayBehindServices, schedGroup);
-    }
-
-    BroadcastQueueImpl(ActivityManagerService service, Handler handler,
-            String name, BroadcastConstants constants, BroadcastSkipPolicy skipPolicy,
-            BroadcastHistory history, boolean allowDelayBehindServices, int schedGroup) {
-        super(service, handler, name, skipPolicy, history);
-        mHandler = new BroadcastHandler(handler.getLooper());
-        mConstants = constants;
-        mDelayBehindServices = allowDelayBehindServices;
-        mSchedGroup = schedGroup;
-        mDispatcher = new BroadcastDispatcher(this, mConstants, mHandler, mService);
-    }
-
-    public void start(ContentResolver resolver) {
-        mDispatcher.start();
-        mConstants.startObserving(mHandler, resolver);
-    }
-
-    public boolean isDelayBehindServices() {
-        return mDelayBehindServices;
-    }
-
-    public BroadcastRecord getPendingBroadcastLocked() {
-        return mPendingBroadcast;
-    }
-
-    public BroadcastRecord getActiveBroadcastLocked() {
-        return mDispatcher.getActiveBroadcastLocked();
-    }
-
-    public int getPreferredSchedulingGroupLocked(ProcessRecord app) {
-        final BroadcastRecord active = getActiveBroadcastLocked();
-        if (active != null && active.curApp == app) {
-            return mSchedGroup;
-        }
-        final BroadcastRecord pending = getPendingBroadcastLocked();
-        if (pending != null && pending.curApp == app) {
-            return mSchedGroup;
-        }
-        return ProcessList.SCHED_GROUP_UNDEFINED;
-    }
-
-    public void enqueueBroadcastLocked(BroadcastRecord r) {
-        r.applySingletonPolicy(mService);
-
-        final boolean replacePending = (r.intent.getFlags()
-                & Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
-
-        // Ordered broadcasts obviously need to be dispatched in serial order,
-        // but this implementation expects all manifest receivers to also be
-        // dispatched in a serial fashion
-        boolean serialDispatch = r.ordered;
-        if (!serialDispatch) {
-            final int N = (r.receivers != null) ? r.receivers.size() : 0;
-            for (int i = 0; i < N; i++) {
-                if (r.receivers.get(i) instanceof ResolveInfo) {
-                    serialDispatch = true;
-                    break;
-                }
-            }
-        }
-
-        if (serialDispatch) {
-            final BroadcastRecord oldRecord =
-                    replacePending ? replaceOrderedBroadcastLocked(r) : null;
-            if (oldRecord != null) {
-                // Replaced, fire the result-to receiver.
-                if (oldRecord.resultTo != null) {
-                    try {
-                        oldRecord.mIsReceiverAppRunning = true;
-                        performReceiveLocked(oldRecord, oldRecord.resultToApp, oldRecord.resultTo,
-                                oldRecord.intent,
-                                Activity.RESULT_CANCELED, null, null,
-                                false, false, oldRecord.shareIdentity, oldRecord.userId,
-                                oldRecord.callingUid, r.callingUid, r.callerPackage,
-                                SystemClock.uptimeMillis() - oldRecord.enqueueTime, 0, 0,
-                                oldRecord.resultToApp != null
-                                        ? oldRecord.resultToApp.mState.getCurProcState()
-                                        : ActivityManager.PROCESS_STATE_UNKNOWN);
-                    } catch (RemoteException e) {
-                        Slog.w(TAG, "Failure ["
-                                + mQueueName + "] sending broadcast result of "
-                                + oldRecord.intent, e);
-
-                    }
-                }
-            } else {
-                enqueueOrderedBroadcastLocked(r);
-                scheduleBroadcastsLocked();
-            }
-        } else {
-            final boolean replaced = replacePending
-                    && (replaceParallelBroadcastLocked(r) != null);
-            // Note: We assume resultTo is null for non-ordered broadcasts.
-            if (!replaced) {
-                enqueueParallelBroadcastLocked(r);
-                scheduleBroadcastsLocked();
-            }
-        }
-    }
-
-    public void enqueueParallelBroadcastLocked(BroadcastRecord r) {
-        r.enqueueClockTime = System.currentTimeMillis();
-        r.enqueueTime = SystemClock.uptimeMillis();
-        r.enqueueRealTime = SystemClock.elapsedRealtime();
-        mParallelBroadcasts.add(r);
-        enqueueBroadcastHelper(r);
-    }
-
-    public void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
-        r.enqueueClockTime = System.currentTimeMillis();
-        r.enqueueTime = SystemClock.uptimeMillis();
-        r.enqueueRealTime = SystemClock.elapsedRealtime();
-        mDispatcher.enqueueOrderedBroadcastLocked(r);
-        enqueueBroadcastHelper(r);
-    }
-
-    /**
-     * Don't call this method directly; call enqueueParallelBroadcastLocked or
-     * enqueueOrderedBroadcastLocked.
-     */
-    private void enqueueBroadcastHelper(BroadcastRecord r) {
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
-            Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_PENDING),
-                System.identityHashCode(r));
-        }
-    }
-
-    /**
-     * Find the same intent from queued parallel broadcast, replace with a new one and return
-     * the old one.
-     */
-    public final BroadcastRecord replaceParallelBroadcastLocked(BroadcastRecord r) {
-        return replaceBroadcastLocked(mParallelBroadcasts, r, "PARALLEL");
-    }
-
-    /**
-     * Find the same intent from queued ordered broadcast, replace with a new one and return
-     * the old one.
-     */
-    public final BroadcastRecord replaceOrderedBroadcastLocked(BroadcastRecord r) {
-        return mDispatcher.replaceBroadcastLocked(r, "ORDERED");
-    }
-
-    private BroadcastRecord replaceBroadcastLocked(ArrayList<BroadcastRecord> queue,
-            BroadcastRecord r, String typeForLogging) {
-        final Intent intent = r.intent;
-        for (int i = queue.size() - 1; i >= 0; i--) {
-            final BroadcastRecord old = queue.get(i);
-            if (old.userId == r.userId && intent.filterEquals(old.intent)) {
-                if (DEBUG_BROADCAST) {
-                    Slog.v(TAG_BROADCAST, "***** DROPPING "
-                            + typeForLogging + " [" + mQueueName + "]: " + intent);
-                }
-                queue.set(i, r);
-                return old;
-            }
-        }
-        return null;
-    }
-
-    private final void processCurBroadcastLocked(BroadcastRecord r,
-            ProcessRecord app) throws RemoteException {
-        if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                "Process cur broadcast " + r + " for app " + app);
-        final IApplicationThread thread = app.getThread();
-        if (thread == null) {
-            throw new RemoteException();
-        }
-        if (app.isInFullBackup()) {
-            skipReceiverLocked(r);
-            return;
-        }
-
-        r.curApp = app;
-        r.curAppLastProcessState = app.mState.getCurProcState();
-        final ProcessReceiverRecord prr = app.mReceivers;
-        prr.addCurReceiver(r);
-        app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
-        // Don't bump its LRU position if it's in the background restricted.
-        if (mService.mInternal.getRestrictionLevel(app.info.packageName, app.userId)
-                < RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
-            mService.updateLruProcessLocked(app, false, null);
-        }
-        // Make sure the oom adj score is updated before delivering the broadcast.
-        // Force an update, even if there are other pending requests, overall it still saves time,
-        // because time(updateOomAdj(N apps)) <= N * time(updateOomAdj(1 app)).
-        mService.enqueueOomAdjTargetLocked(app);
-        mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER);
-
-        // Tell the application to launch this receiver.
-        maybeReportBroadcastDispatchedEventLocked(r, r.curReceiver.applicationInfo.uid);
-        r.intent.setComponent(r.curComponent);
-
-        // See if we need to delay the freezer based on BroadcastOptions
-        if (r.options != null
-                && r.options.getTemporaryAppAllowlistDuration() > 0
-                && r.options.getTemporaryAppAllowlistType()
-                    == TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED) {
-            mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(app,
-                    CachedAppOptimizer.UNFREEZE_REASON_START_RECEIVER,
-                    r.options.getTemporaryAppAllowlistDuration());
-        }
-
-        boolean started = false;
-        try {
-            if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
-                    "Delivering to component " + r.curComponent
-                    + ": " + r);
-            mService.notifyPackageUse(r.intent.getComponent().getPackageName(),
-                                      PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);
-            final boolean assumeDelivered = false;
-            thread.scheduleReceiver(
-                    prepareReceiverIntent(r.intent, r.curFilteredExtras),
-                    r.curReceiver, null /* compatInfo (unused but need to keep method signature) */,
-                    r.resultCode, r.resultData, r.resultExtras, r.ordered, assumeDelivered,
-                    r.userId, r.shareIdentity ? r.callingUid : Process.INVALID_UID,
-                    app.mState.getReportedProcState(),
-                    r.shareIdentity ? r.callerPackage : null);
-            if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                    "Process cur broadcast " + r + " DELIVERED for app " + app);
-            started = true;
-        } finally {
-            if (!started) {
-                if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                        "Process cur broadcast " + r + ": NOT STARTED!");
-                r.curApp = null;
-                r.curAppLastProcessState = ActivityManager.PROCESS_STATE_UNKNOWN;
-                prr.removeCurReceiver(r);
-            }
-        }
-
-        // if something bad happens here, launch the app and try again
-        if (app.isKilled()) {
-            throw new RemoteException("app gets killed during broadcasting");
-        }
-    }
-
-    /**
-     * Called by ActivityManagerService to notify that the uid has process started, if there is any
-     * deferred BOOT_COMPLETED broadcast, the BroadcastDispatcher can dispatch the broadcast now.
-     * @param uid
-     */
-    public void updateUidReadyForBootCompletedBroadcastLocked(int uid) {
-        mDispatcher.updateUidReadyForBootCompletedBroadcastLocked(uid);
-        scheduleBroadcastsLocked();
-    }
-
-    public boolean onApplicationAttachedLocked(ProcessRecord app)
-            throws BroadcastDeliveryFailedException {
-        updateUidReadyForBootCompletedBroadcastLocked(app.uid);
-
-        if (mPendingBroadcast != null && mPendingBroadcast.curApp == app) {
-            return sendPendingBroadcastsLocked(app);
-        } else {
-            return false;
-        }
-    }
-
-    public void onApplicationTimeoutLocked(ProcessRecord app) {
-        skipCurrentOrPendingReceiverLocked(app);
-    }
-
-    public void onApplicationProblemLocked(ProcessRecord app) {
-        skipCurrentOrPendingReceiverLocked(app);
-    }
-
-    public void onApplicationCleanupLocked(ProcessRecord app) {
-        skipCurrentOrPendingReceiverLocked(app);
-    }
-
-    public void onProcessFreezableChangedLocked(ProcessRecord app) {
-        // Not supported; ignore
-    }
-
-    public boolean sendPendingBroadcastsLocked(ProcessRecord app)
-            throws BroadcastDeliveryFailedException {
-        boolean didSomething = false;
-        final BroadcastRecord br = mPendingBroadcast;
-        if (br != null && br.curApp.getPid() > 0 && br.curApp.getPid() == app.getPid()) {
-            if (br.curApp != app) {
-                Slog.e(TAG, "App mismatch when sending pending broadcast to "
-                        + app.processName + ", intended target is " + br.curApp.processName);
-                return false;
-            }
-            try {
-                mPendingBroadcast = null;
-                br.mIsReceiverAppRunning = false;
-                processCurBroadcastLocked(br, app);
-                didSomething = true;
-            } catch (Exception e) {
-                Slog.w(TAG, "Exception in new application when starting receiver "
-                        + br.curComponent.flattenToShortString(), e);
-                logBroadcastReceiverDiscardLocked(br);
-                finishReceiverLocked(br, br.resultCode, br.resultData,
-                        br.resultExtras, br.resultAbort, false);
-                scheduleBroadcastsLocked();
-                // We need to reset the state if we failed to start the receiver.
-                br.state = BroadcastRecord.IDLE;
-                throw new BroadcastDeliveryFailedException(e);
-            }
-        }
-        return didSomething;
-    }
-
-    // Skip the current receiver, if any, that is in flight to the given process
-    public boolean skipCurrentOrPendingReceiverLocked(ProcessRecord app) {
-        BroadcastRecord r = null;
-        final BroadcastRecord curActive = mDispatcher.getActiveBroadcastLocked();
-        if (curActive != null && curActive.curApp == app) {
-            // confirmed: the current active broadcast is to the given app
-            r = curActive;
-        }
-
-        // If the current active broadcast isn't this BUT we're waiting for
-        // mPendingBroadcast to spin up the target app, that's what we use.
-        if (r == null && mPendingBroadcast != null && mPendingBroadcast.curApp == app) {
-            if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
-                    "[" + mQueueName + "] skip & discard pending app " + r);
-            r = mPendingBroadcast;
-        }
-
-        if (r != null) {
-            skipReceiverLocked(r);
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    private void skipReceiverLocked(BroadcastRecord r) {
-        logBroadcastReceiverDiscardLocked(r);
-        finishReceiverLocked(r, r.resultCode, r.resultData,
-                r.resultExtras, r.resultAbort, false);
-        scheduleBroadcastsLocked();
-    }
-
-    public void scheduleBroadcastsLocked() {
-        if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Schedule broadcasts ["
-                + mQueueName + "]: current="
-                + mBroadcastsScheduled);
-
-        if (mBroadcastsScheduled) {
-            return;
-        }
-        mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
-        mBroadcastsScheduled = true;
-    }
-
-    public BroadcastRecord getMatchingOrderedReceiver(ProcessRecord app) {
-        BroadcastRecord br = mDispatcher.getActiveBroadcastLocked();
-        if (br == null) {
-            Slog.w(TAG_BROADCAST, "getMatchingOrderedReceiver [" + mQueueName
-                    + "] no active broadcast");
-            return null;
-        }
-        if (br.curApp != app) {
-            Slog.w(TAG_BROADCAST, "getMatchingOrderedReceiver [" + mQueueName
-                    + "] active broadcast " + br.curApp + " doesn't match " + app);
-            return null;
-        }
-        return br;
-    }
-
-    // > 0 only, no worry about "eventual" recycling
-    private int nextSplitTokenLocked() {
-        int next = mNextToken + 1;
-        if (next <= 0) {
-            next = 1;
-        }
-        mNextToken = next;
-        return next;
-    }
-
-    private void postActivityStartTokenRemoval(ProcessRecord app, BroadcastRecord r) {
-        // the receiver had run for less than allowed bg activity start timeout,
-        // so allow the process to still start activities from bg for some more time
-        String msgToken = (app.toShortString() + r.toString()).intern();
-        // first, if there exists a past scheduled request to remove this token, drop
-        // that request - we don't want the token to be swept from under our feet...
-        mHandler.removeCallbacksAndMessages(msgToken);
-        // ...then schedule the removal of the token after the extended timeout
-        mHandler.postAtTime(() -> {
-            synchronized (mService) {
-                app.removeBackgroundStartPrivileges(r);
-            }
-        }, msgToken, (r.receiverTime + mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT));
-    }
-
-    public boolean finishReceiverLocked(ProcessRecord app, int resultCode,
-            String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
-        final BroadcastRecord r = getMatchingOrderedReceiver(app);
-        if (r != null) {
-            return finishReceiverLocked(r, resultCode,
-                    resultData, resultExtras, resultAbort, waitForServices);
-        } else {
-            return false;
-        }
-    }
-
-    public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,
-            String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
-        final int state = r.state;
-        final ActivityInfo receiver = r.curReceiver;
-        final long finishTime = SystemClock.uptimeMillis();
-        final long elapsed = finishTime - r.receiverTime;
-        r.state = BroadcastRecord.IDLE;
-        final int curIndex = r.nextReceiver - 1;
-
-        final int packageState = r.mWasReceiverAppStopped
-                ? SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED
-                : SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
-
-        if (curIndex >= 0 && curIndex < r.receivers.size() && r.curApp != null) {
-            final Object curReceiver = r.receivers.get(curIndex);
-            FrameworkStatsLog.write(BROADCAST_DELIVERY_EVENT_REPORTED, r.curApp.uid,
-                    r.callingUid == -1 ? Process.SYSTEM_UID : r.callingUid,
-                    r.intent.getAction(),
-                    curReceiver instanceof BroadcastFilter
-                    ? BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME
-                    : BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST,
-                    r.mIsReceiverAppRunning
-                    ? BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM
-                    : BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD,
-                    r.dispatchTime - r.enqueueTime,
-                    r.receiverTime - r.dispatchTime,
-                    finishTime - r.receiverTime,
-                    packageState,
-                    r.curApp.info.packageName,
-                    r.callerPackage,
-                    r.calculateTypeForLogging(),
-                    r.getDeliveryGroupPolicy(),
-                    r.intent.getFlags(),
-                    BroadcastRecord.getReceiverPriority(curReceiver),
-                    r.callerProcState,
-                    r.curAppLastProcessState);
-        }
-        if (state == BroadcastRecord.IDLE) {
-            Slog.w(TAG_BROADCAST, "finishReceiver [" + mQueueName + "] called but state is IDLE");
-        }
-        if (r.mBackgroundStartPrivileges.allowsAny() && r.curApp != null) {
-            if (elapsed > mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT) {
-                // if the receiver has run for more than allowed bg activity start timeout,
-                // just remove the token for this process now and we're done
-                r.curApp.removeBackgroundStartPrivileges(r);
-            } else {
-                // It gets more time; post the removal to happen at the appropriate moment
-                postActivityStartTokenRemoval(r.curApp, r);
-            }
-        }
-        // If we're abandoning this broadcast before any receivers were actually spun up,
-        // nextReceiver is zero; in which case time-to-process bookkeeping doesn't apply.
-        if (r.nextReceiver > 0) {
-            r.terminalTime[r.nextReceiver - 1] = finishTime;
-        }
-
-        // if this receiver was slow, impose deferral policy on the app.  This will kick in
-        // when processNextBroadcastLocked() next finds this uid as a receiver identity.
-        if (!r.timeoutExempt) {
-            // r.curApp can be null if finish has raced with process death - benign
-            // edge case, and we just ignore it because we're already cleaning up
-            // as expected.
-            if (r.curApp != null
-                    && mConstants.SLOW_TIME > 0 && elapsed > mConstants.SLOW_TIME) {
-                // Core system packages are exempt from deferral policy
-                if (!UserHandle.isCore(r.curApp.uid)) {
-                    if (DEBUG_BROADCAST_DEFERRAL) {
-                        Slog.i(TAG_BROADCAST, "Broadcast receiver " + (r.nextReceiver - 1)
-                                + " was slow: " + receiver + " br=" + r);
-                    }
-                    mDispatcher.startDeferring(r.curApp.uid);
-                } else {
-                    if (DEBUG_BROADCAST_DEFERRAL) {
-                        Slog.i(TAG_BROADCAST, "Core uid " + r.curApp.uid
-                                + " receiver was slow but not deferring: "
-                                + receiver + " br=" + r);
-                    }
-                }
-            }
-        } else {
-            if (DEBUG_BROADCAST_DEFERRAL) {
-                Slog.i(TAG_BROADCAST, "Finished broadcast " + r.intent.getAction()
-                        + " is exempt from deferral policy");
-            }
-        }
-
-        r.intent.setComponent(null);
-        if (r.curApp != null && r.curApp.mReceivers.hasCurReceiver(r)) {
-            r.curApp.mReceivers.removeCurReceiver(r);
-            mService.enqueueOomAdjTargetLocked(r.curApp);
-        }
-        if (r.curFilter != null) {
-            r.curFilter.receiverList.curBroadcast = null;
-        }
-        r.curFilter = null;
-        r.curReceiver = null;
-        r.curApp = null;
-        r.curAppLastProcessState = ActivityManager.PROCESS_STATE_UNKNOWN;
-        r.curFilteredExtras = null;
-        r.mWasReceiverAppStopped = false;
-        mPendingBroadcast = null;
-
-        r.resultCode = resultCode;
-        r.resultData = resultData;
-        r.resultExtras = resultExtras;
-        if (resultAbort && (r.intent.getFlags()&Intent.FLAG_RECEIVER_NO_ABORT) == 0) {
-            r.resultAbort = resultAbort;
-        } else {
-            r.resultAbort = false;
-        }
-
-        // If we want to wait behind services *AND* we're finishing the head/
-        // active broadcast on its queue
-        if (waitForServices && r.curComponent != null && r.queue.isDelayBehindServices()
-                && ((BroadcastQueueImpl) r.queue).getActiveBroadcastLocked() == r) {
-            ActivityInfo nextReceiver;
-            if (r.nextReceiver < r.receivers.size()) {
-                Object obj = r.receivers.get(r.nextReceiver);
-                nextReceiver = (obj instanceof ActivityInfo) ? (ActivityInfo)obj : null;
-            } else {
-                nextReceiver = null;
-            }
-            // Don't do this if the next receive is in the same process as the current one.
-            if (receiver == null || nextReceiver == null
-                    || receiver.applicationInfo.uid != nextReceiver.applicationInfo.uid
-                    || !receiver.processName.equals(nextReceiver.processName)) {
-                // In this case, we are ready to process the next receiver for the current broadcast,
-                // but are on a queue that would like to wait for services to finish before moving
-                // on.  If there are background services currently starting, then we will go into a
-                // special state where we hold off on continuing this broadcast until they are done.
-                if (mService.mServices.hasBackgroundServicesLocked(r.userId)) {
-                    Slog.i(TAG, "Delay finish: " + r.curComponent.flattenToShortString());
-                    r.state = BroadcastRecord.WAITING_SERVICES;
-                    return false;
-                }
-            }
-        }
-
-        r.curComponent = null;
-
-        // We will process the next receiver right now if this is finishing
-        // an app receiver (which is always asynchronous) or after we have
-        // come back from calling a receiver.
-        final boolean doNext = (state == BroadcastRecord.APP_RECEIVE)
-                || (state == BroadcastRecord.CALL_DONE_RECEIVE);
-        if (doNext) {
-            processNextBroadcastLocked(/* fromMsg= */ false, /* skipOomAdj= */ true);
-        }
-        return doNext;
-    }
-
-    public void backgroundServicesFinishedLocked(int userId) {
-        BroadcastRecord br = mDispatcher.getActiveBroadcastLocked();
-        if (br != null) {
-            if (br.userId == userId && br.state == BroadcastRecord.WAITING_SERVICES) {
-                Slog.i(TAG, "Resuming delayed broadcast");
-                br.curComponent = null;
-                br.state = BroadcastRecord.IDLE;
-                processNextBroadcastLocked(false, false);
-            }
-        }
-    }
-
-    public void performReceiveLocked(BroadcastRecord r, ProcessRecord app, IIntentReceiver receiver,
-            Intent intent, int resultCode, String data, Bundle extras,
-            boolean ordered, boolean sticky, boolean shareIdentity, int sendingUser,
-            int receiverUid, int callingUid, String callingPackage,
-            long dispatchDelay, long receiveDelay, int priority,
-            int receiverProcessState) throws RemoteException {
-        // If the broadcaster opted-in to sharing their identity, then expose package visibility for
-        // the receiver.
-        if (shareIdentity) {
-            mService.mPackageManagerInt.grantImplicitAccess(sendingUser, intent,
-                    UserHandle.getAppId(receiverUid), callingUid, true);
-        }
-        // Send the intent to the receiver asynchronously using one-way binder calls.
-        if (app != null) {
-            final IApplicationThread thread = app.getThread();
-            if (thread != null) {
-                // If we have an app thread, do the call through that so it is
-                // correctly ordered with other one-way calls.
-                try {
-                    final boolean assumeDelivered = !ordered;
-                    thread.scheduleRegisteredReceiver(
-                            receiver, intent, resultCode,
-                            data, extras, ordered, sticky, assumeDelivered, sendingUser,
-                            app.mState.getReportedProcState(),
-                            shareIdentity ? callingUid : Process.INVALID_UID,
-                            shareIdentity ? callingPackage : null);
-                } catch (RemoteException ex) {
-                    // Failed to call into the process. It's either dying or wedged. Kill it gently.
-                    synchronized (mService) {
-                        final String msg = "Failed to schedule " + intent + " to " + receiver
-                                + " via " + app + ": " + ex;
-                        Slog.w(TAG, msg);
-                        app.killLocked("Can't deliver broadcast", ApplicationExitInfo.REASON_OTHER,
-                                ApplicationExitInfo.SUBREASON_UNDELIVERED_BROADCAST, true);
-                    }
-                    throw ex;
-                }
-            } else {
-                // Application has died. Receiver doesn't exist.
-                throw new RemoteException("app.thread must not be null");
-            }
-        } else {
-            receiver.performReceive(intent, resultCode, data, extras, ordered,
-                    sticky, sendingUser);
-        }
-        if (!ordered) {
-            FrameworkStatsLog.write(BROADCAST_DELIVERY_EVENT_REPORTED,
-                    receiverUid == -1 ? Process.SYSTEM_UID : receiverUid,
-                    callingUid == -1 ? Process.SYSTEM_UID : callingUid,
-                    intent.getAction(),
-                    BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME,
-                    BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM,
-                    dispatchDelay, receiveDelay, 0 /* finish_delay */,
-                    SERVICE_REQUEST_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL,
-                    app != null ? app.info.packageName : null, callingPackage,
-                    r.calculateTypeForLogging(), r.getDeliveryGroupPolicy(), r.intent.getFlags(),
-                    priority, r.callerProcState, receiverProcessState);
-        }
-    }
-
-    private void deliverToRegisteredReceiverLocked(BroadcastRecord r,
-            BroadcastFilter filter, boolean ordered, int index) {
-        boolean skip = mSkipPolicy.shouldSkip(r, filter);
-
-        // Filter packages in the intent extras, skipping delivery if none of the packages is
-        // visible to the receiver.
-        Bundle filteredExtras = null;
-        if (!skip && r.filterExtrasForReceiver != null) {
-            final Bundle extras = r.intent.getExtras();
-            if (extras != null) {
-                filteredExtras = r.filterExtrasForReceiver.apply(filter.receiverList.uid, extras);
-                if (filteredExtras == null) {
-                    if (DEBUG_BROADCAST) {
-                        Slog.v(TAG, "Skipping delivery to "
-                                + filter.receiverList.app
-                                + " : receiver is filtered by the package visibility");
-                    }
-                    skip = true;
-                }
-            }
-        }
-
-        if (skip) {
-            r.delivery[index] = BroadcastRecord.DELIVERY_SKIPPED;
-            return;
-        }
-
-        r.delivery[index] = BroadcastRecord.DELIVERY_DELIVERED;
-
-        // If this is not being sent as an ordered broadcast, then we
-        // don't want to touch the fields that keep track of the current
-        // state of ordered broadcasts.
-        if (ordered) {
-            r.curFilter = filter;
-            filter.receiverList.curBroadcast = r;
-            r.state = BroadcastRecord.CALL_IN_RECEIVE;
-            if (filter.receiverList.app != null) {
-                // Bump hosting application to no longer be in background
-                // scheduling class.  Note that we can't do that if there
-                // isn't an app...  but we can only be in that case for
-                // things that directly call the IActivityManager API, which
-                // are already core system stuff so don't matter for this.
-                r.curApp = filter.receiverList.app;
-                r.curAppLastProcessState = r.curApp.mState.getCurProcState();
-                filter.receiverList.app.mReceivers.addCurReceiver(r);
-                mService.enqueueOomAdjTargetLocked(r.curApp);
-                mService.updateOomAdjPendingTargetsLocked(
-                        OOM_ADJ_REASON_START_RECEIVER);
-            }
-        } else if (filter.receiverList.app != null) {
-            mService.mOomAdjuster.unfreezeTemporarily(filter.receiverList.app,
-                    CachedAppOptimizer.UNFREEZE_REASON_START_RECEIVER);
-        }
-
-        try {
-            if (DEBUG_BROADCAST_LIGHT) Slog.i(TAG_BROADCAST,
-                    "Delivering to " + filter + " : " + r);
-            final boolean isInFullBackup = (filter.receiverList.app != null)
-                    && filter.receiverList.app.isInFullBackup();
-            final boolean isKilled = (filter.receiverList.app != null)
-                    && filter.receiverList.app.isKilled();
-            if (isInFullBackup || isKilled) {
-                // Skip delivery if full backup in progress
-                // If it's an ordered broadcast, we need to continue to the next receiver.
-                if (ordered) {
-                    skipReceiverLocked(r);
-                }
-            } else {
-                r.receiverTime = SystemClock.uptimeMillis();
-                r.scheduledTime[index] = r.receiverTime;
-                maybeAddBackgroundStartPrivileges(filter.receiverList.app, r);
-                maybeScheduleTempAllowlistLocked(filter.owningUid, r, r.options);
-                maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);
-                performReceiveLocked(r, filter.receiverList.app, filter.receiverList.receiver,
-                        prepareReceiverIntent(r.intent, filteredExtras), r.resultCode, r.resultData,
-                        r.resultExtras, r.ordered, r.initialSticky, r.shareIdentity, r.userId,
-                        filter.receiverList.uid, r.callingUid, r.callerPackage,
-                        r.dispatchTime - r.enqueueTime,
-                        r.receiverTime - r.dispatchTime, filter.getPriority(),
-                        filter.receiverList.app != null
-                                ? filter.receiverList.app.mState.getCurProcState()
-                                : ActivityManager.PROCESS_STATE_UNKNOWN);
-                // parallel broadcasts are fire-and-forget, not bookended by a call to
-                // finishReceiverLocked(), so we manage their activity-start token here
-                if (filter.receiverList.app != null
-                        && r.mBackgroundStartPrivileges.allowsAny()
-                        && !r.ordered) {
-                    postActivityStartTokenRemoval(filter.receiverList.app, r);
-                }
-            }
-            if (ordered) {
-                r.state = BroadcastRecord.CALL_DONE_RECEIVE;
-            }
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Failure sending broadcast " + r.intent, e);
-            // Clean up ProcessRecord state related to this broadcast attempt
-            if (filter.receiverList.app != null) {
-                filter.receiverList.app.removeBackgroundStartPrivileges(r);
-                if (ordered) {
-                    filter.receiverList.app.mReceivers.removeCurReceiver(r);
-                    // Something wrong, its oom adj could be downgraded, but not in a hurry.
-                    mService.enqueueOomAdjTargetLocked(r.curApp);
-                }
-            }
-            // And BroadcastRecord state related to ordered delivery, if appropriate
-            if (ordered) {
-                r.curFilter = null;
-                filter.receiverList.curBroadcast = null;
-            }
-        }
-    }
-
-    void maybeScheduleTempAllowlistLocked(int uid, BroadcastRecord r,
-            @Nullable BroadcastOptions brOptions) {
-        if (brOptions == null || brOptions.getTemporaryAppAllowlistDuration() <= 0) {
-            return;
-        }
-        long duration = brOptions.getTemporaryAppAllowlistDuration();
-        final @TempAllowListType int type = brOptions.getTemporaryAppAllowlistType();
-        final @ReasonCode int reasonCode = brOptions.getTemporaryAppAllowlistReasonCode();
-        final String reason = brOptions.getTemporaryAppAllowlistReason();
-
-        if (duration > Integer.MAX_VALUE) {
-            duration = Integer.MAX_VALUE;
-        }
-        // XXX ideally we should pause the broadcast until everything behind this is done,
-        // or else we will likely start dispatching the broadcast before we have opened
-        // access to the app (there is a lot of asynchronicity behind this).  It is probably
-        // not that big a deal, however, because the main purpose here is to allow apps
-        // to hold wake locks, and they will be able to acquire their wake lock immediately
-        // it just won't be enabled until we get through this work.
-        StringBuilder b = new StringBuilder();
-        b.append("broadcast:");
-        UserHandle.formatUid(b, r.callingUid);
-        b.append(":");
-        if (r.intent.getAction() != null) {
-            b.append(r.intent.getAction());
-        } else if (r.intent.getComponent() != null) {
-            r.intent.getComponent().appendShortString(b);
-        } else if (r.intent.getData() != null) {
-            b.append(r.intent.getData());
-        }
-        b.append(",reason:");
-        b.append(reason);
-        if (DEBUG_BROADCAST) {
-            Slog.v(TAG, "Broadcast temp allowlist uid=" + uid + " duration=" + duration
-                    + " type=" + type + " : " + b.toString());
-        }
-
-        // Only add to temp allowlist if it's not the APP_FREEZING_DELAYED type. That will be
-        // handled when the broadcast is actually being scheduled on the app thread.
-        if (type != TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED) {
-            mService.tempAllowlistUidLocked(uid, duration, reasonCode, b.toString(), type,
-                    r.callingUid);
-        }
-    }
-
-    private void processNextBroadcast(boolean fromMsg) {
-        synchronized (mService) {
-            processNextBroadcastLocked(fromMsg, false);
-        }
-    }
-
-    private static Intent prepareReceiverIntent(@NonNull Intent originalIntent,
-            @Nullable Bundle filteredExtras) {
-        final Intent intent = new Intent(originalIntent);
-        if (filteredExtras != null) {
-            intent.replaceExtras(filteredExtras);
-        }
-        return intent;
-    }
-
-    public void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
-        BroadcastRecord r;
-
-        if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "processNextBroadcast ["
-                + mQueueName + "]: "
-                + mParallelBroadcasts.size() + " parallel broadcasts; "
-                + mDispatcher.describeStateLocked());
-
-        mService.updateCpuStats();
-
-        if (fromMsg) {
-            mBroadcastsScheduled = false;
-        }
-
-        // First, deliver any non-serialized broadcasts right away.
-        while (mParallelBroadcasts.size() > 0) {
-            r = mParallelBroadcasts.remove(0);
-            r.dispatchTime = SystemClock.uptimeMillis();
-            r.dispatchRealTime = SystemClock.elapsedRealtime();
-            r.dispatchClockTime = System.currentTimeMillis();
-            r.mIsReceiverAppRunning = true;
-
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
-                Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                    createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_PENDING),
-                    System.identityHashCode(r));
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                    createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_DELIVERED),
-                    System.identityHashCode(r));
-            }
-
-            final int N = r.receivers.size();
-            if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Processing parallel broadcast ["
-                    + mQueueName + "] " + r);
-            for (int i=0; i<N; i++) {
-                Object target = r.receivers.get(i);
-                if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                        "Delivering non-ordered on [" + mQueueName + "] to registered "
-                        + target + ": " + r);
-                deliverToRegisteredReceiverLocked(r,
-                        (BroadcastFilter) target, false, i);
-            }
-            addBroadcastToHistoryLocked(r);
-            if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Done with parallel broadcast ["
-                    + mQueueName + "] " + r);
-        }
-
-        // Now take care of the next serialized one...
-
-        // If we are waiting for a process to come up to handle the next
-        // broadcast, then do nothing at this point.  Just in case, we
-        // check that the process we're waiting for still exists.
-        if (mPendingBroadcast != null) {
-            if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
-                    "processNextBroadcast [" + mQueueName + "]: waiting for "
-                    + mPendingBroadcast.curApp);
-
-            boolean isDead;
-            if (mPendingBroadcast.curApp.getPid() > 0) {
-                synchronized (mService.mPidsSelfLocked) {
-                    ProcessRecord proc = mService.mPidsSelfLocked.get(
-                            mPendingBroadcast.curApp.getPid());
-                    isDead = proc == null || proc.mErrorState.isCrashing();
-                }
-            } else {
-                final ProcessRecord proc = mService.mProcessList.getProcessNamesLOSP().get(
-                        mPendingBroadcast.curApp.processName, mPendingBroadcast.curApp.uid);
-                isDead = proc == null || !proc.isPendingStart();
-            }
-            if (!isDead) {
-                // It's still alive, so keep waiting
-                return;
-            } else {
-                Slog.w(TAG, "pending app  ["
-                        + mQueueName + "]" + mPendingBroadcast.curApp
-                        + " died before responding to broadcast");
-                mPendingBroadcast.state = BroadcastRecord.IDLE;
-                mPendingBroadcast.nextReceiver = mPendingBroadcastRecvIndex;
-                mPendingBroadcast = null;
-            }
-        }
-
-        boolean looped = false;
-
-        do {
-            final long now = SystemClock.uptimeMillis();
-            r = mDispatcher.getNextBroadcastLocked(now);
-
-            if (r == null) {
-                // No more broadcasts are deliverable right now, so all done!
-                mDispatcher.scheduleDeferralCheckLocked(false);
-                synchronized (mService.mAppProfiler.mProfilerLock) {
-                    mService.mAppProfiler.scheduleAppGcsLPf();
-                }
-                if (looped && !skipOomAdj) {
-                    // If we had finished the last ordered broadcast, then
-                    // make sure all processes have correct oom and sched
-                    // adjustments.
-                    mService.updateOomAdjPendingTargetsLocked(
-                            OOM_ADJ_REASON_START_RECEIVER);
-                }
-
-                // when we have no more ordered broadcast on this queue, stop logging
-                if (mService.mUserController.mBootCompleted && mLogLatencyMetrics) {
-                    mLogLatencyMetrics = false;
-                }
-
-                return;
-            }
-
-            boolean forceReceive = false;
-
-            // Ensure that even if something goes awry with the timeout
-            // detection, we catch "hung" broadcasts here, discard them,
-            // and continue to make progress.
-            //
-            // This is only done if the system is ready so that early-stage receivers
-            // don't get executed with timeouts; and of course other timeout-
-            // exempt broadcasts are ignored.
-            int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
-            if (mService.mProcessesReady && !r.timeoutExempt && r.dispatchTime > 0) {
-                if ((numReceivers > 0) &&
-                        (now > r.dispatchTime + (2 * mConstants.TIMEOUT * numReceivers))) {
-                    Slog.w(TAG, "Hung broadcast ["
-                            + mQueueName + "] discarded after timeout failure:"
-                            + " now=" + now
-                            + " dispatchTime=" + r.dispatchTime
-                            + " startTime=" + r.receiverTime
-                            + " intent=" + r.intent
-                            + " numReceivers=" + numReceivers
-                            + " nextReceiver=" + r.nextReceiver
-                            + " state=" + r.state);
-                    broadcastTimeoutLocked(false); // forcibly finish this broadcast
-                    forceReceive = true;
-                    r.state = BroadcastRecord.IDLE;
-                }
-            }
-
-            if (r.state != BroadcastRecord.IDLE) {
-                if (DEBUG_BROADCAST) Slog.d(TAG_BROADCAST,
-                        "processNextBroadcast("
-                        + mQueueName + ") called when not idle (state="
-                        + r.state + ")");
-                return;
-            }
-
-            // Is the current broadcast is done for any reason?
-            if (r.receivers == null || r.nextReceiver >= numReceivers
-                    || r.resultAbort || forceReceive) {
-                // Send the final result if requested
-                if (r.resultTo != null) {
-                    boolean sendResult = true;
-
-                    // if this was part of a split/deferral complex, update the refcount and only
-                    // send the completion when we clear all of them
-                    if (r.splitToken != 0) {
-                        int newCount = mSplitRefcounts.get(r.splitToken) - 1;
-                        if (newCount == 0) {
-                            // done!  clear out this record's bookkeeping and deliver
-                            if (DEBUG_BROADCAST_DEFERRAL) {
-                                Slog.i(TAG_BROADCAST,
-                                        "Sending broadcast completion for split token "
-                                        + r.splitToken + " : " + r.intent.getAction());
-                            }
-                            mSplitRefcounts.delete(r.splitToken);
-                        } else {
-                            // still have some split broadcast records in flight; update refcount
-                            // and hold off on the callback
-                            if (DEBUG_BROADCAST_DEFERRAL) {
-                                Slog.i(TAG_BROADCAST,
-                                        "Result refcount now " + newCount + " for split token "
-                                        + r.splitToken + " : " + r.intent.getAction()
-                                        + " - not sending completion yet");
-                            }
-                            sendResult = false;
-                            mSplitRefcounts.put(r.splitToken, newCount);
-                        }
-                    }
-                    if (sendResult) {
-                        if (r.callerApp != null) {
-                            mService.mOomAdjuster.unfreezeTemporarily(
-                                    r.callerApp,
-                                    CachedAppOptimizer.UNFREEZE_REASON_FINISH_RECEIVER);
-                        }
-                        try {
-                            if (DEBUG_BROADCAST) {
-                                Slog.i(TAG_BROADCAST, "Finishing broadcast [" + mQueueName + "] "
-                                        + r.intent.getAction() + " app=" + r.callerApp);
-                            }
-                            if (r.dispatchTime == 0) {
-                                // The dispatch time here could be 0, in case it's a parallel
-                                // broadcast but it has a result receiver. Set it to now.
-                                r.dispatchTime = now;
-                            }
-                            r.mIsReceiverAppRunning = true;
-                            performReceiveLocked(r, r.resultToApp, r.resultTo,
-                                    new Intent(r.intent), r.resultCode,
-                                    r.resultData, r.resultExtras, false, false, r.shareIdentity,
-                                    r.userId, r.callingUid, r.callingUid, r.callerPackage,
-                                    r.dispatchTime - r.enqueueTime,
-                                    now - r.dispatchTime, 0,
-                                    r.resultToApp != null
-                                            ? r.resultToApp.mState.getCurProcState()
-                                            : ActivityManager.PROCESS_STATE_UNKNOWN);
-                            logBootCompletedBroadcastCompletionLatencyIfPossible(r);
-                            // Set this to null so that the reference
-                            // (local and remote) isn't kept in the mBroadcastHistory.
-                            r.resultTo = null;
-                        } catch (RemoteException e) {
-                            r.resultTo = null;
-                            Slog.w(TAG, "Failure ["
-                                    + mQueueName + "] sending broadcast result of "
-                                    + r.intent, e);
-                        }
-                    }
-                }
-
-                if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Cancelling BROADCAST_TIMEOUT_MSG");
-                cancelBroadcastTimeoutLocked();
-
-                if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
-                        "Finished with ordered broadcast " + r);
-
-                // ... and on to the next...
-                addBroadcastToHistoryLocked(r);
-                if (r.intent.getComponent() == null && r.intent.getPackage() == null
-                        && (r.intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
-                    // This was an implicit broadcast... let's record it for posterity.
-                    mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
-                            r.manifestCount, r.manifestSkipCount, r.finishTime-r.dispatchTime);
-                }
-                mDispatcher.retireBroadcastLocked(r);
-                r = null;
-                looped = true;
-                continue;
-            }
-
-            // Check whether the next receiver is under deferral policy, and handle that
-            // accordingly.  If the current broadcast was already part of deferred-delivery
-            // tracking, we know that it must now be deliverable as-is without re-deferral.
-            if (!r.deferred) {
-                final int receiverUid = r.getReceiverUid(r.receivers.get(r.nextReceiver));
-                if (mDispatcher.isDeferringLocked(receiverUid)) {
-                    if (DEBUG_BROADCAST_DEFERRAL) {
-                        Slog.i(TAG_BROADCAST, "Next receiver in " + r + " uid " + receiverUid
-                                + " at " + r.nextReceiver + " is under deferral");
-                    }
-                    // If this is the only (remaining) receiver in the broadcast, "splitting"
-                    // doesn't make sense -- just defer it as-is and retire it as the
-                    // currently active outgoing broadcast.
-                    BroadcastRecord defer;
-                    if (r.nextReceiver + 1 == numReceivers) {
-                        if (DEBUG_BROADCAST_DEFERRAL) {
-                            Slog.i(TAG_BROADCAST, "Sole receiver of " + r
-                                    + " is under deferral; setting aside and proceeding");
-                        }
-                        defer = r;
-                        mDispatcher.retireBroadcastLocked(r);
-                    } else {
-                        // Nontrivial case; split out 'uid's receivers to a new broadcast record
-                        // and defer that, then loop and pick up continuing delivery of the current
-                        // record (now absent those receivers).
-
-                        // The split operation is guaranteed to match at least at 'nextReceiver'
-                        defer = r.splitRecipientsLocked(receiverUid, r.nextReceiver);
-                        if (DEBUG_BROADCAST_DEFERRAL) {
-                            Slog.i(TAG_BROADCAST, "Post split:");
-                            Slog.i(TAG_BROADCAST, "Original broadcast receivers:");
-                            for (int i = 0; i < r.receivers.size(); i++) {
-                                Slog.i(TAG_BROADCAST, "  " + r.receivers.get(i));
-                            }
-                            Slog.i(TAG_BROADCAST, "Split receivers:");
-                            for (int i = 0; i < defer.receivers.size(); i++) {
-                                Slog.i(TAG_BROADCAST, "  " + defer.receivers.get(i));
-                            }
-                        }
-                        // Track completion refcount as well if relevant
-                        if (r.resultTo != null) {
-                            int token = r.splitToken;
-                            if (token == 0) {
-                                // first split of this record; refcount for 'r' and 'deferred'
-                                r.splitToken = defer.splitToken = nextSplitTokenLocked();
-                                mSplitRefcounts.put(r.splitToken, 2);
-                                if (DEBUG_BROADCAST_DEFERRAL) {
-                                    Slog.i(TAG_BROADCAST,
-                                            "Broadcast needs split refcount; using new token "
-                                            + r.splitToken);
-                                }
-                            } else {
-                                // new split from an already-refcounted situation; increment count
-                                final int curCount = mSplitRefcounts.get(token);
-                                if (DEBUG_BROADCAST_DEFERRAL) {
-                                    if (curCount == 0) {
-                                        Slog.wtf(TAG_BROADCAST,
-                                                "Split refcount is zero with token for " + r);
-                                    }
-                                }
-                                mSplitRefcounts.put(token, curCount + 1);
-                                if (DEBUG_BROADCAST_DEFERRAL) {
-                                    Slog.i(TAG_BROADCAST, "New split count for token " + token
-                                            + " is " + (curCount + 1));
-                                }
-                            }
-                        }
-                    }
-                    mDispatcher.addDeferredBroadcast(receiverUid, defer);
-                    r = null;
-                    looped = true;
-                    continue;
-                }
-            }
-        } while (r == null);
-
-        // Get the next receiver...
-        int recIdx = r.nextReceiver++;
-
-        // Keep track of when this receiver started, and make sure there
-        // is a timeout message pending to kill it if need be.
-        r.receiverTime = SystemClock.uptimeMillis();
-        r.scheduledTime[recIdx] = r.receiverTime;
-        if (recIdx == 0) {
-            r.dispatchTime = r.receiverTime;
-            r.dispatchRealTime = SystemClock.elapsedRealtime();
-            r.dispatchClockTime = System.currentTimeMillis();
-
-            if (mLogLatencyMetrics) {
-                FrameworkStatsLog.write(
-                        FrameworkStatsLog.BROADCAST_DISPATCH_LATENCY_REPORTED,
-                        r.dispatchClockTime - r.enqueueClockTime);
-            }
-
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
-                Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                    createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_PENDING),
-                    System.identityHashCode(r));
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                    createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_DELIVERED),
-                    System.identityHashCode(r));
-            }
-            if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Processing ordered broadcast ["
-                    + mQueueName + "] " + r);
-        }
-        if (! mPendingBroadcastTimeoutMessage) {
-            long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
-            if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
-                    "Submitting BROADCAST_TIMEOUT_MSG ["
-                    + mQueueName + "] for " + r + " at " + timeoutTime);
-            setBroadcastTimeoutLocked(timeoutTime);
-        }
-
-        final BroadcastOptions brOptions = r.options;
-        final Object nextReceiver = r.receivers.get(recIdx);
-
-        if (nextReceiver instanceof BroadcastFilter) {
-            // Simple case: this is a registered receiver who gets
-            // a direct call.
-            BroadcastFilter filter = (BroadcastFilter)nextReceiver;
-            if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                    "Delivering ordered ["
-                    + mQueueName + "] to registered "
-                    + filter + ": " + r);
-            r.mIsReceiverAppRunning = true;
-            deliverToRegisteredReceiverLocked(r, filter, r.ordered, recIdx);
-            if ((r.curReceiver == null && r.curFilter == null) || !r.ordered) {
-                // The receiver has already finished, so schedule to
-                // process the next one.
-                if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Quick finishing ["
-                        + mQueueName + "]: ordered=" + r.ordered
-                        + " curFilter=" + r.curFilter
-                        + " curReceiver=" + r.curReceiver);
-                r.state = BroadcastRecord.IDLE;
-                scheduleBroadcastsLocked();
-            } else {
-                if (filter.receiverList != null) {
-                    maybeAddBackgroundStartPrivileges(filter.receiverList.app, r);
-                    // r is guaranteed ordered at this point, so we know finishReceiverLocked()
-                    // will get a callback and handle the activity start token lifecycle.
-                }
-            }
-            return;
-        }
-
-        // Hard case: need to instantiate the receiver, possibly
-        // starting its application process to host it.
-
-        final ResolveInfo info =
-            (ResolveInfo)nextReceiver;
-        final ComponentName component = new ComponentName(
-                info.activityInfo.applicationInfo.packageName,
-                info.activityInfo.name);
-        final int receiverUid = info.activityInfo.applicationInfo.uid;
-
-        final String targetProcess = info.activityInfo.processName;
-        final ProcessRecord app = mService.getProcessRecordLocked(targetProcess,
-                info.activityInfo.applicationInfo.uid);
-
-        boolean skip = mSkipPolicy.shouldSkip(r, info);
-
-        // Filter packages in the intent extras, skipping delivery if none of the packages is
-        // visible to the receiver.
-        Bundle filteredExtras = null;
-        if (!skip && r.filterExtrasForReceiver != null) {
-            final Bundle extras = r.intent.getExtras();
-            if (extras != null) {
-                filteredExtras = r.filterExtrasForReceiver.apply(receiverUid, extras);
-                if (filteredExtras == null) {
-                    if (DEBUG_BROADCAST) {
-                        Slog.v(TAG, "Skipping delivery to "
-                                + info.activityInfo.packageName + " / " + receiverUid
-                                + " : receiver is filtered by the package visibility");
-                    }
-                    skip = true;
-                }
-            }
-        }
-
-        if (skip) {
-            if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                    "Skipping delivery of ordered [" + mQueueName + "] "
-                    + r + " for reason described above");
-            r.delivery[recIdx] = BroadcastRecord.DELIVERY_SKIPPED;
-            r.curFilter = null;
-            r.state = BroadcastRecord.IDLE;
-            r.manifestSkipCount++;
-            scheduleBroadcastsLocked();
-            return;
-        }
-        r.manifestCount++;
-
-        r.delivery[recIdx] = BroadcastRecord.DELIVERY_DELIVERED;
-        r.state = BroadcastRecord.APP_RECEIVE;
-        r.curComponent = component;
-        r.curReceiver = info.activityInfo;
-        r.curFilteredExtras = filteredExtras;
-        if (DEBUG_MU && r.callingUid > UserHandle.PER_USER_RANGE) {
-            Slog.v(TAG_MU, "Updated broadcast record activity info for secondary user, "
-                    + info.activityInfo + ", callingUid = " + r.callingUid + ", uid = "
-                    + receiverUid);
-        }
-        final boolean isActivityCapable =
-                (brOptions != null && brOptions.getTemporaryAppAllowlistDuration() > 0);
-        maybeScheduleTempAllowlistLocked(receiverUid, r, brOptions);
-
-        // Report that a component is used for explicit broadcasts.
-        if (r.intent.getComponent() != null && r.curComponent != null
-                && !TextUtils.equals(r.curComponent.getPackageName(), r.callerPackage)) {
-            mService.mUsageStatsService.reportEvent(
-                    r.curComponent.getPackageName(), r.userId, Event.APP_COMPONENT_USED);
-        }
-
-        try {
-            mService.mPackageManagerInt.notifyComponentUsed(
-                    r.curComponent.getPackageName(), r.userId, r.callerPackage, r.toString());
-        } catch (IllegalArgumentException e) {
-            Slog.w(TAG, "Failed trying to unstop package "
-                    + r.curComponent.getPackageName() + ": " + e);
-        }
-
-        // Is this receiver's application already running?
-        if (app != null && app.getThread() != null && !app.isKilled()) {
-            try {
-                app.addPackage(info.activityInfo.packageName,
-                        info.activityInfo.applicationInfo.longVersionCode, mService.mProcessStats);
-                maybeAddBackgroundStartPrivileges(app, r);
-                r.mIsReceiverAppRunning = true;
-                processCurBroadcastLocked(r, app);
-                return;
-            } catch (RemoteException e) {
-                final String msg = "Failed to schedule " + r.intent + " to " + info
-                        + " via " + app + ": " + e;
-                Slog.w(TAG, msg);
-                app.killLocked("Can't deliver broadcast", ApplicationExitInfo.REASON_OTHER,
-                        ApplicationExitInfo.SUBREASON_UNDELIVERED_BROADCAST, true);
-            } catch (RuntimeException e) {
-                Slog.wtf(TAG, "Failed sending broadcast to "
-                        + r.curComponent + " with " + r.intent, e);
-                // If some unexpected exception happened, just skip
-                // this broadcast.  At this point we are not in the call
-                // from a client, so throwing an exception out from here
-                // will crash the entire system instead of just whoever
-                // sent the broadcast.
-                logBroadcastReceiverDiscardLocked(r);
-                finishReceiverLocked(r, r.resultCode, r.resultData,
-                        r.resultExtras, r.resultAbort, false);
-                scheduleBroadcastsLocked();
-                // We need to reset the state if we failed to start the receiver.
-                r.state = BroadcastRecord.IDLE;
-                return;
-            }
-
-            // If a dead object exception was thrown -- fall through to
-            // restart the application.
-        }
-
-        // Registered whether we're bringing this package out of a stopped state
-        r.mWasReceiverAppStopped =
-                (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0;
-        // Not running -- get it started, to be executed when the app comes up.
-        if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
-                "Need to start app ["
-                + mQueueName + "] " + targetProcess + " for broadcast " + r);
-        r.curApp = mService.startProcessLocked(targetProcess,
-                info.activityInfo.applicationInfo, true,
-                r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
-                new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent,
-                        r.intent.getAction(), r.getHostingRecordTriggerType()),
-                isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY,
-                (r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false);
-        r.curAppLastProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
-        if (r.curApp == null) {
-            // Ah, this recipient is unavailable.  Finish it if necessary,
-            // and mark the broadcast record as ready for the next.
-            Slog.w(TAG, "Unable to launch app "
-                    + info.activityInfo.applicationInfo.packageName + "/"
-                    + receiverUid + " for broadcast "
-                    + r.intent + ": process is bad");
-            logBroadcastReceiverDiscardLocked(r);
-            finishReceiverLocked(r, r.resultCode, r.resultData,
-                    r.resultExtras, r.resultAbort, false);
-            scheduleBroadcastsLocked();
-            r.state = BroadcastRecord.IDLE;
-            return;
-        }
-
-        maybeAddBackgroundStartPrivileges(r.curApp, r);
-        mPendingBroadcast = r;
-        mPendingBroadcastRecvIndex = recIdx;
-    }
-
-    @Nullable
-    private String getTargetPackage(BroadcastRecord r) {
-        if (r.intent == null) {
-            return null;
-        }
-        if (r.intent.getPackage() != null) {
-            return r.intent.getPackage();
-        } else if (r.intent.getComponent() != null) {
-            return r.intent.getComponent().getPackageName();
-        }
-        return null;
-    }
-
-    static void logBootCompletedBroadcastCompletionLatencyIfPossible(BroadcastRecord r) {
-        // Only log after last receiver.
-        // In case of split BOOT_COMPLETED broadcast, make sure only call this method on the
-        // last BroadcastRecord of the split broadcast which has non-null resultTo.
-        final int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
-        if (r.nextReceiver < numReceivers) {
-            return;
-        }
-        final String action = r.intent.getAction();
-        int event = 0;
-        if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
-            event = BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__LOCKED_BOOT_COMPLETED;
-        } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
-            event = BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__BOOT_COMPLETED;
-        }
-        if (event != 0) {
-            final int dispatchLatency = (int)(r.dispatchTime - r.enqueueTime);
-            final int completeLatency = (int)
-                    (SystemClock.uptimeMillis() - r.enqueueTime);
-            final int dispatchRealLatency = (int)(r.dispatchRealTime - r.enqueueRealTime);
-            final int completeRealLatency = (int)
-                    (SystemClock.elapsedRealtime() - r.enqueueRealTime);
-            int userType = FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN;
-            // This method is called very infrequently, no performance issue we call
-            // LocalServices.getService() here.
-            final UserManagerInternal umInternal = LocalServices.getService(
-                    UserManagerInternal.class);
-            final UserInfo userInfo =
-                    (umInternal != null) ? umInternal.getUserInfo(r.userId) : null;
-            if (userInfo != null) {
-                userType = UserJourneyLogger.getUserTypeForStatsd(userInfo.userType);
-            }
-            Slog.i(TAG_BROADCAST,
-                    "BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED action:"
-                            + action
-                            + " dispatchLatency:" + dispatchLatency
-                            + " completeLatency:" + completeLatency
-                            + " dispatchRealLatency:" + dispatchRealLatency
-                            + " completeRealLatency:" + completeRealLatency
-                            + " receiversSize:" + numReceivers
-                            + " userId:" + r.userId
-                            + " userType:" + (userInfo != null? userInfo.userType : null));
-            FrameworkStatsLog.write(
-                    BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED,
-                    event,
-                    dispatchLatency,
-                    completeLatency,
-                    dispatchRealLatency,
-                    completeRealLatency,
-                    r.userId,
-                    userType);
-        }
-    }
-
-    private void maybeReportBroadcastDispatchedEventLocked(BroadcastRecord r, int targetUid) {
-        if (r.options == null || r.options.getIdForResponseEvent() <= 0) {
-            return;
-        }
-        final String targetPackage = getTargetPackage(r);
-        // Ignore non-explicit broadcasts
-        if (targetPackage == null) {
-            return;
-        }
-        mService.mUsageStatsService.reportBroadcastDispatched(
-                r.callingUid, targetPackage, UserHandle.of(r.userId),
-                r.options.getIdForResponseEvent(), SystemClock.elapsedRealtime(),
-                mService.getUidStateLocked(targetUid));
-    }
-
-    private void maybeAddBackgroundStartPrivileges(ProcessRecord proc, BroadcastRecord r) {
-        if (r == null || proc == null || !r.mBackgroundStartPrivileges.allowsAny()) {
-            return;
-        }
-        String msgToken = (proc.toShortString() + r.toString()).intern();
-        // first, if there exists a past scheduled request to remove this token, drop
-        // that request - we don't want the token to be swept from under our feet...
-        mHandler.removeCallbacksAndMessages(msgToken);
-        // ...then add the token
-        proc.addOrUpdateBackgroundStartPrivileges(r, r.mBackgroundStartPrivileges);
-    }
-
-    final void setBroadcastTimeoutLocked(long timeoutTime) {
-        if (! mPendingBroadcastTimeoutMessage) {
-            Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
-            mHandler.sendMessageAtTime(msg, timeoutTime);
-            mPendingBroadcastTimeoutMessage = true;
-        }
-    }
-
-    final void cancelBroadcastTimeoutLocked() {
-        if (mPendingBroadcastTimeoutMessage) {
-            mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
-            mPendingBroadcastTimeoutMessage = false;
-        }
-    }
-
-    final void broadcastTimeoutLocked(boolean fromMsg) {
-        if (fromMsg) {
-            mPendingBroadcastTimeoutMessage = false;
-        }
-
-        if (mDispatcher.isEmpty() || mDispatcher.getActiveBroadcastLocked() == null) {
-            return;
-        }
-        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastTimeoutLocked()");
-        try {
-            long now = SystemClock.uptimeMillis();
-            BroadcastRecord r = mDispatcher.getActiveBroadcastLocked();
-            if (fromMsg) {
-                if (!mService.mProcessesReady) {
-                    // Only process broadcast timeouts if the system is ready; some early
-                    // broadcasts do heavy work setting up system facilities
-                    return;
-                }
-
-                // If the broadcast is generally exempt from timeout tracking, we're done
-                if (r.timeoutExempt) {
-                    if (DEBUG_BROADCAST) {
-                        Slog.i(TAG_BROADCAST, "Broadcast timeout but it's exempt: "
-                                + r.intent.getAction());
-                    }
-                    return;
-                }
-                long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
-                if (timeoutTime > now) {
-                    // We can observe premature timeouts because we do not cancel and reset the
-                    // broadcast timeout message after each receiver finishes.  Instead, we set up
-                    // an initial timeout then kick it down the road a little further as needed
-                    // when it expires.
-                    if (DEBUG_BROADCAST) {
-                        Slog.v(TAG_BROADCAST,
-                                "Premature timeout ["
-                                + mQueueName + "] @ " + now
-                                + ": resetting BROADCAST_TIMEOUT_MSG for "
-                                + timeoutTime);
-                    }
-                    setBroadcastTimeoutLocked(timeoutTime);
-                    return;
-                }
-            }
-
-            if (r.state == BroadcastRecord.WAITING_SERVICES) {
-                // In this case the broadcast had already finished, but we had decided to wait
-                // for started services to finish as well before going on.  So if we have actually
-                // waited long enough time timeout the broadcast, let's give up on the whole thing
-                // and just move on to the next.
-                Slog.i(TAG, "Waited long enough for: " + (r.curComponent != null
-                        ? r.curComponent.flattenToShortString() : "(null)"));
-                r.curComponent = null;
-                r.state = BroadcastRecord.IDLE;
-                processNextBroadcastLocked(false, false);
-                return;
-            }
-
-            // If the receiver app is being debugged we quietly ignore unresponsiveness, just
-            // tidying up and moving on to the next broadcast without crashing or ANRing this
-            // app just because it's stopped at a breakpoint.
-            final boolean debugging = (r.curApp != null && r.curApp.isDebugging());
-
-            long timeoutDurationMs = now - r.receiverTime;
-            Slog.w(TAG, "Timeout of broadcast " + r + " - curFilter=" + r.curFilter
-                    + " curReceiver=" + r.curReceiver + ", started " + timeoutDurationMs
-                    + "ms ago");
-            r.receiverTime = now;
-            if (!debugging) {
-                r.anrCount++;
-            }
-
-            ProcessRecord app = null;
-            Object curReceiver;
-            if (r.nextReceiver > 0) {
-                curReceiver = r.receivers.get(r.nextReceiver - 1);
-                r.delivery[r.nextReceiver - 1] = BroadcastRecord.DELIVERY_TIMEOUT;
-            } else {
-                curReceiver = r.curReceiver;
-            }
-            Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver);
-            logBroadcastReceiverDiscardLocked(r);
-            TimeoutRecord timeoutRecord = TimeoutRecord.forBroadcastReceiver(r.intent,
-                    timeoutDurationMs);
-            if (curReceiver != null && curReceiver instanceof BroadcastFilter) {
-                BroadcastFilter bf = (BroadcastFilter) curReceiver;
-                if (bf.receiverList.pid != 0
-                        && bf.receiverList.pid != ActivityManagerService.MY_PID) {
-                    timeoutRecord.mLatencyTracker.waitingOnPidLockStarted();
-                    synchronized (mService.mPidsSelfLocked) {
-                        timeoutRecord.mLatencyTracker.waitingOnPidLockEnded();
-                        app = mService.mPidsSelfLocked.get(
-                                bf.receiverList.pid);
-                    }
-                }
-            } else {
-                app = r.curApp;
-            }
-
-            if (mPendingBroadcast == r) {
-                mPendingBroadcast = null;
-            }
-
-            // Move on to the next receiver.
-            finishReceiverLocked(r, r.resultCode, r.resultData,
-                    r.resultExtras, r.resultAbort, false);
-            scheduleBroadcastsLocked();
-
-            // The ANR should only be triggered if we have a process record (app is non-null)
-            if (!debugging && app != null) {
-                mService.appNotResponding(app, timeoutRecord);
-            }
-
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-        }
-
-    }
-
-    private final void addBroadcastToHistoryLocked(BroadcastRecord original) {
-        if (original.callingUid < 0) {
-            // This was from a registerReceiver() call; ignore it.
-            return;
-        }
-        original.finishTime = SystemClock.uptimeMillis();
-
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
-            Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                createBroadcastTraceTitle(original, BroadcastRecord.DELIVERY_DELIVERED),
-                System.identityHashCode(original));
-        }
-
-        mService.notifyBroadcastFinishedLocked(original);
-        mHistory.addBroadcastToHistoryLocked(original);
-    }
-
-    public boolean cleanupDisabledPackageReceiversLocked(
-            String packageName, Set<String> filterByClasses, int userId) {
-        boolean didSomething = false;
-        for (int i = mParallelBroadcasts.size() - 1; i >= 0; i--) {
-            didSomething |= mParallelBroadcasts.get(i).cleanupDisabledPackageReceiversLocked(
-                    packageName, filterByClasses, userId, true);
-        }
-
-        didSomething |= mDispatcher.cleanupDisabledPackageReceiversLocked(packageName,
-                filterByClasses, userId, true);
-
-        return didSomething;
-    }
-
-    final void logBroadcastReceiverDiscardLocked(BroadcastRecord r) {
-        final int logIndex = r.nextReceiver - 1;
-        if (logIndex >= 0 && logIndex < r.receivers.size()) {
-            Object curReceiver = r.receivers.get(logIndex);
-            if (curReceiver instanceof BroadcastFilter) {
-                BroadcastFilter bf = (BroadcastFilter) curReceiver;
-                EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_FILTER,
-                        bf.owningUserId, System.identityHashCode(r),
-                        r.intent.getAction(), logIndex, System.identityHashCode(bf));
-            } else {
-                ResolveInfo ri = (ResolveInfo) curReceiver;
-                EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_APP,
-                        UserHandle.getUserId(ri.activityInfo.applicationInfo.uid),
-                        System.identityHashCode(r), r.intent.getAction(), logIndex, ri.toString());
-            }
-        } else {
-            if (logIndex < 0) Slog.w(TAG,
-                    "Discarding broadcast before first receiver is invoked: " + r);
-            EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_APP,
-                    -1, System.identityHashCode(r),
-                    r.intent.getAction(),
-                    r.nextReceiver,
-                    "NONE");
-        }
-    }
-
-    private String createBroadcastTraceTitle(BroadcastRecord record, int state) {
-        return formatSimple("Broadcast %s from %s (%s) %s",
-                state == BroadcastRecord.DELIVERY_PENDING ? "in queue" : "dispatched",
-                record.callerPackage == null ? "" : record.callerPackage,
-                record.callerApp == null ? "process unknown" : record.callerApp.toShortString(),
-                record.intent == null ? "" : record.intent.getAction());
-    }
-
-    public boolean isIdleLocked() {
-        return mParallelBroadcasts.isEmpty() && mDispatcher.isIdle()
-                && (mPendingBroadcast == null);
-    }
-
-    public boolean isBeyondBarrierLocked(long barrierTime) {
-        // If nothing active, we're beyond barrier
-        if (isIdleLocked()) return true;
-
-        // Check if parallel broadcasts are beyond barrier
-        for (int i = 0; i < mParallelBroadcasts.size(); i++) {
-            if (mParallelBroadcasts.get(i).enqueueTime <= barrierTime) {
-                return false;
-            }
-        }
-
-        // Check if pending broadcast is beyond barrier
-        final BroadcastRecord pending = getPendingBroadcastLocked();
-        if ((pending != null) && pending.enqueueTime <= barrierTime) {
-            return false;
-        }
-
-        return mDispatcher.isBeyondBarrier(barrierTime);
-    }
-
-    public boolean isDispatchedLocked(Intent intent) {
-        if (isIdleLocked()) return true;
-
-        for (int i = 0; i < mParallelBroadcasts.size(); i++) {
-            if (intent.filterEquals(mParallelBroadcasts.get(i).intent)) {
-                return false;
-            }
-        }
-
-        final BroadcastRecord pending = getPendingBroadcastLocked();
-        if ((pending != null) && intent.filterEquals(pending.intent)) {
-            return false;
-        }
-
-        return mDispatcher.isDispatched(intent);
-    }
-
-    public void waitForIdle(PrintWriter pw) {
-        waitFor(() -> isIdleLocked(), pw, "idle");
-    }
-
-    public void waitForBarrier(PrintWriter pw) {
-        final long barrierTime = SystemClock.uptimeMillis();
-        waitFor(() -> isBeyondBarrierLocked(barrierTime), pw, "barrier");
-    }
-
-    public void waitForDispatched(Intent intent, PrintWriter pw) {
-        waitFor(() -> isDispatchedLocked(intent), pw, "dispatch");
-    }
-
-    private void waitFor(BooleanSupplier condition, PrintWriter pw, String conditionName) {
-        long lastPrint = 0;
-        while (true) {
-            synchronized (mService) {
-                if (condition.getAsBoolean()) {
-                    final String msg = "Queue [" + mQueueName + "] reached " + conditionName
-                            + " condition";
-                    Slog.v(TAG, msg);
-                    if (pw != null) {
-                        pw.println(msg);
-                        pw.flush();
-                    }
-                    return;
-                }
-            }
-
-            // Print at most every second
-            final long now = SystemClock.uptimeMillis();
-            if (now >= lastPrint + 1000) {
-                lastPrint = now;
-                final String msg = "Queue [" + mQueueName + "] waiting for " + conditionName
-                        + " condition; state is " + describeStateLocked();
-                Slog.v(TAG, msg);
-                if (pw != null) {
-                    pw.println(msg);
-                    pw.flush();
-                }
-            }
-
-            // Push through any deferrals to try meeting our condition
-            cancelDeferrals();
-            SystemClock.sleep(100);
-        }
-    }
-
-    // Used by wait-for-broadcast-idle : fast-forward all current deferrals to
-    // be immediately deliverable.
-    public void cancelDeferrals() {
-        synchronized (mService) {
-            mDispatcher.cancelDeferralsLocked();
-            scheduleBroadcastsLocked();
-        }
-    }
-
-    public String describeStateLocked() {
-        return mParallelBroadcasts.size() + " parallel; "
-                + mDispatcher.describeStateLocked();
-    }
-
-    @NeverCompile
-    public void dumpDebug(ProtoOutputStream proto, long fieldId) {
-        long token = proto.start(fieldId);
-        proto.write(BroadcastQueueProto.QUEUE_NAME, mQueueName);
-        int N;
-        N = mParallelBroadcasts.size();
-        for (int i = N - 1; i >= 0; i--) {
-            mParallelBroadcasts.get(i).dumpDebug(proto, BroadcastQueueProto.PARALLEL_BROADCASTS);
-        }
-        mDispatcher.dumpDebug(proto, BroadcastQueueProto.ORDERED_BROADCASTS);
-        if (mPendingBroadcast != null) {
-            mPendingBroadcast.dumpDebug(proto, BroadcastQueueProto.PENDING_BROADCAST);
-        }
-        mHistory.dumpDebug(proto);
-        proto.end(token);
-    }
-
-    @NeverCompile
-    public boolean dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args,
-            int opti, boolean dumpConstants, boolean dumpHistory, boolean dumpAll,
-            String dumpPackage, boolean needSep) {
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-        if (!mParallelBroadcasts.isEmpty() || !mDispatcher.isEmpty()
-                || mPendingBroadcast != null) {
-            boolean printed = false;
-            for (int i = mParallelBroadcasts.size() - 1; i >= 0; i--) {
-                BroadcastRecord br = mParallelBroadcasts.get(i);
-                if (dumpPackage != null && !dumpPackage.equals(br.callerPackage)) {
-                    continue;
-                }
-                if (!printed) {
-                    if (needSep) {
-                        pw.println();
-                    }
-                    needSep = true;
-                    printed = true;
-                    pw.println("  Active broadcasts [" + mQueueName + "]:");
-                }
-                pw.println("  Active Broadcast " + mQueueName + " #" + i + ":");
-                br.dump(pw, "    ", sdf);
-            }
-
-            mDispatcher.dumpLocked(pw, dumpPackage, mQueueName, sdf);
-
-            if (dumpPackage == null || (mPendingBroadcast != null
-                    && dumpPackage.equals(mPendingBroadcast.callerPackage))) {
-                pw.println();
-                pw.println("  Pending broadcast [" + mQueueName + "]:");
-                if (mPendingBroadcast != null) {
-                    mPendingBroadcast.dump(pw, "    ", sdf);
-                } else {
-                    pw.println("    (null)");
-                }
-                needSep = true;
-            }
-        }
-        if (dumpConstants) {
-            mConstants.dump(new IndentingPrintWriter(pw));
-        }
-        if (dumpHistory) {
-            needSep = mHistory.dumpLocked(pw, dumpPackage, mQueueName, sdf, dumpAll, needSep);
-        }
-        return needSep;
-    }
-}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 98263df..4422608 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -20,6 +20,9 @@
 import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
 import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
 
+import static com.android.internal.util.FrameworkStatsLog.BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED;
+import static com.android.internal.util.FrameworkStatsLog.BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__BOOT_COMPLETED;
+import static com.android.internal.util.FrameworkStatsLog.BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__LOCKED_BOOT_COMPLETED;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_UNKNOWN;
@@ -59,6 +62,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
 import android.os.Bundle;
 import android.os.BundleMerger;
 import android.os.Handler;
@@ -85,9 +89,12 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.os.TimeoutRecord;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.LocalServices;
 import com.android.server.am.BroadcastProcessQueue.BroadcastConsumer;
 import com.android.server.am.BroadcastProcessQueue.BroadcastPredicate;
 import com.android.server.am.BroadcastRecord.DeliveryState;
+import com.android.server.pm.UserJourneyLogger;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.utils.AnrTimer;
 
 import dalvik.annotation.optimization.NeverCompile;
@@ -2120,7 +2127,7 @@
         r.nextReceiver = r.receivers.size();
         mHistory.onBroadcastFinishedLocked(r);
 
-        BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
+        logBootCompletedBroadcastCompletionLatencyIfPossible(r);
 
         if (r.intent.getComponent() == null && r.intent.getPackage() == null
                 && (r.intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
@@ -2216,6 +2223,59 @@
         return null;
     }
 
+    private void logBootCompletedBroadcastCompletionLatencyIfPossible(BroadcastRecord r) {
+        // Only log after last receiver.
+        // In case of split BOOT_COMPLETED broadcast, make sure only call this method on the
+        // last BroadcastRecord of the split broadcast which has non-null resultTo.
+        final int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
+        if (r.nextReceiver < numReceivers) {
+            return;
+        }
+        final String action = r.intent.getAction();
+        int event = 0;
+        if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
+            event = BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__LOCKED_BOOT_COMPLETED;
+        } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+            event = BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED__EVENT__BOOT_COMPLETED;
+        }
+        if (event != 0) {
+            final int dispatchLatency = (int) (r.dispatchTime - r.enqueueTime);
+            final int completeLatency = (int) (SystemClock.uptimeMillis() - r.enqueueTime);
+            final int dispatchRealLatency = (int) (r.dispatchRealTime - r.enqueueRealTime);
+            final int completeRealLatency = (int)
+                    (SystemClock.elapsedRealtime() - r.enqueueRealTime);
+            int userType =
+                    FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN;
+            // This method is called very infrequently, no performance issue we call
+            // LocalServices.getService() here.
+            final UserManagerInternal umInternal = LocalServices.getService(
+                    UserManagerInternal.class);
+            final UserInfo userInfo =
+                    (umInternal != null) ? umInternal.getUserInfo(r.userId) : null;
+            if (userInfo != null) {
+                userType = UserJourneyLogger.getUserTypeForStatsd(userInfo.userType);
+            }
+            Slog.i(TAG, "BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED action:"
+                    + action
+                    + " dispatchLatency:" + dispatchLatency
+                    + " completeLatency:" + completeLatency
+                    + " dispatchRealLatency:" + dispatchRealLatency
+                    + " completeRealLatency:" + completeRealLatency
+                    + " receiversSize:" + numReceivers
+                    + " userId:" + r.userId
+                    + " userType:" + (userInfo != null ? userInfo.userType : null));
+            FrameworkStatsLog.write(
+                    BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED,
+                    event,
+                    dispatchLatency,
+                    completeLatency,
+                    dispatchRealLatency,
+                    completeRealLatency,
+                    r.userId,
+                    userType);
+        }
+    }
+
     @Override
     @NeverCompile
     public void dumpDebug(@NonNull ProtoOutputStream proto, long fieldId) {
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 4ef31bf..f2b9b25 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ProcessList.UNKNOWN_ADJ;
 
 import android.annotation.Nullable;
 import android.app.IServiceConnection;
@@ -37,7 +38,7 @@
 /**
  * Description of a single binding to a service.
  */
-final class ConnectionRecord {
+final class ConnectionRecord implements OomAdjusterModernImpl.Connection{
     final AppBindRecord binding;    // The application/service binding.
     final ActivityServiceConnectionsHolder<ConnectionRecord> activity;  // If non-null, the owning activity.
     final IServiceConnection conn;  // The client connection.
@@ -127,6 +128,14 @@
         aliasComponent = _aliasComponent;
     }
 
+    @Override
+    public void computeHostOomAdjLSP(OomAdjuster oomAdjuster, ProcessRecord host,
+            ProcessRecord client, long now, ProcessRecord topApp, boolean doingAll,
+            int oomAdjReason, int cachedAdj) {
+        oomAdjuster.computeServiceHostOomAdjLSP(this, host, client, now, topApp, doingAll, false,
+                false, oomAdjReason, UNKNOWN_ADJ, false, false);
+    }
+
     public long getFlags() {
         return flags;
     }
diff --git a/services/core/java/com/android/server/am/ContentProviderConnection.java b/services/core/java/com/android/server/am/ContentProviderConnection.java
index 825b938..3988277 100644
--- a/services/core/java/com/android/server/am/ContentProviderConnection.java
+++ b/services/core/java/com/android/server/am/ContentProviderConnection.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROVIDER;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ProcessList.UNKNOWN_ADJ;
 
 import android.annotation.UserIdInt;
 import android.os.Binder;
@@ -32,7 +33,8 @@
 /**
  * Represents a link between a content provider and client.
  */
-public final class ContentProviderConnection extends Binder {
+public final class ContentProviderConnection extends Binder implements
+        OomAdjusterModernImpl.Connection {
     public final ContentProviderRecord provider;
     public final ProcessRecord client;
     public final String clientPackage;
@@ -72,6 +74,14 @@
         createTime = SystemClock.elapsedRealtime();
     }
 
+    @Override
+    public void computeHostOomAdjLSP(OomAdjuster oomAdjuster, ProcessRecord host,
+            ProcessRecord client, long now, ProcessRecord topApp, boolean doingAll,
+            int oomAdjReason, int cachedAdj) {
+        oomAdjuster.computeProviderHostOomAdjLSP(this, host, client, now, topApp, doingAll, false,
+                false, oomAdjReason, UNKNOWN_ADJ, false, false);
+    }
+
     public void startAssociationIfNeeded() {
         // If we don't already have an active association, create one...  but only if this
         // is an association between two different processes.
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 31328ae..cd6964e 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -587,7 +587,7 @@
     }
 
     @GuardedBy({"mService", "mProcLock"})
-    private void performUpdateOomAdjLSP(@OomAdjReason int oomAdjReason) {
+    protected void performUpdateOomAdjLSP(@OomAdjReason int oomAdjReason) {
         final ProcessRecord topApp = mService.getTopApp();
         // Clear any pending ones because we are doing a full update now.
         mPendingProcessSet.clear();
@@ -913,7 +913,7 @@
     }
 
     @GuardedBy("mService")
-    private void performUpdateOomAdjPendingTargetsLocked(@OomAdjReason int oomAdjReason) {
+    protected void performUpdateOomAdjPendingTargetsLocked(@OomAdjReason int oomAdjReason) {
         final ProcessRecord topApp = mService.getTopApp();
 
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
@@ -941,7 +941,7 @@
      * must have called {@link collectReachableProcessesLocked} on it.
      */
     @GuardedBy({"mService", "mProcLock"})
-    protected void updateOomAdjInnerLSP(@OomAdjReason int oomAdjReason, final ProcessRecord topApp,
+    private void updateOomAdjInnerLSP(@OomAdjReason int oomAdjReason, final ProcessRecord topApp,
             ArrayList<ProcessRecord> processes, ActiveUids uids, boolean potentialCycles,
             boolean startProfiling) {
         final boolean fullUpdate = processes == null;
@@ -1775,12 +1775,11 @@
             state.setAdjSeq(mAdjSeq);
             state.setCurrentSchedulingGroup(SCHED_GROUP_BACKGROUND);
             state.setCurProcState(PROCESS_STATE_CACHED_EMPTY);
+            state.setCurRawProcState(PROCESS_STATE_CACHED_EMPTY);
             state.setCurAdj(CACHED_APP_MAX_ADJ);
             state.setCurRawAdj(CACHED_APP_MAX_ADJ);
             state.setCompletedAdjSeq(state.getAdjSeq());
             state.setCurCapability(PROCESS_CAPABILITY_NONE);
-            onProcessStateChanged(app, prevProcState);
-            onProcessOomAdjChanged(app, prevAppAdj);
             return false;
         }
 
@@ -1843,8 +1842,6 @@
             state.setCurRawProcState(state.getCurProcState());
             state.setCurAdj(state.getMaxAdj());
             state.setCompletedAdjSeq(state.getAdjSeq());
-            onProcessStateChanged(app, prevProcState);
-            onProcessOomAdjChanged(app, prevAppAdj);
             // if curAdj is less than prevAppAdj, then this process was promoted
             return state.getCurAdj() < prevAppAdj || state.getCurProcState() < prevProcState;
         }
@@ -2561,7 +2558,7 @@
     }
 
     @GuardedBy({"mService", "mProcLock"})
-    protected boolean computeServiceHostOomAdjLSP(ConnectionRecord cr, ProcessRecord app,
+    public boolean computeServiceHostOomAdjLSP(ConnectionRecord cr, ProcessRecord app,
             ProcessRecord client, long now, ProcessRecord topApp, boolean doingAll,
             boolean cycleReEval, boolean computeClients, int oomAdjReason, int cachedAdj,
             boolean couldRecurse, boolean dryRun) {
@@ -2991,7 +2988,11 @@
         return updated;
     }
 
-    protected boolean computeProviderHostOomAdjLSP(ContentProviderConnection conn,
+    /**
+     * Computes the impact on {@code app} the provider connections from {@code client} has.
+     */
+    @GuardedBy({"mService", "mProcLock"})
+    public boolean computeProviderHostOomAdjLSP(ContentProviderConnection conn,
             ProcessRecord app, ProcessRecord client, long now, ProcessRecord topApp,
             boolean doingAll, boolean cycleReEval, boolean computeClients, int oomAdjReason,
             int cachedAdj, boolean couldRecurse, boolean dryRun) {
@@ -3572,7 +3573,7 @@
         int initialCapability =  PROCESS_CAPABILITY_NONE;
         boolean initialCached = true;
         final ProcessStateRecord state = app.mState;
-        final int prevProcState = state.getCurRawProcState();
+        final int prevProcState = state.getCurProcState();
         final int prevAdj = state.getCurRawAdj();
         // If the process has been marked as foreground, it is starting as the top app (with
         // Zygote#START_AS_TOP_APP_ARG), so boost the thread priority of its default UI thread.
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 1bf779a..dd75bc0 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -38,7 +38,6 @@
 import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_OOM_ADJ;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_UID_OBSERVERS;
 import static com.android.server.am.ActivityManagerService.TAG_UID_OBSERVERS;
 import static com.android.server.am.ProcessList.BACKUP_APP_ADJ;
@@ -67,8 +66,10 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal.OomAdjReason;
 import android.content.pm.ServiceInfo;
+import android.os.IBinder;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
 
@@ -80,6 +81,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 /**
@@ -275,6 +277,7 @@
                 mProcessRecordNodes[i] = new LinkedProcessRecordList(type);
             }
             mLastNode = new ProcessRecordNode[size];
+            resetLastNodes();
         }
 
         int size() {
@@ -285,7 +288,7 @@
         void reset() {
             for (int i = 0; i < mProcessRecordNodes.length; i++) {
                 mProcessRecordNodes[i].reset();
-                mLastNode[i] = null;
+                setLastNodeToHead(i);
             }
         }
 
@@ -509,26 +512,118 @@
     }
 
     /**
-     * A helper consumer for collecting processes that have not been reached yet. To avoid object
-     * allocations every OomAdjuster update, the results will be stored in
-     * {@link UnreachedProcessCollector#processList}. The process list reader is responsible
-     * for setting it before usage, as well as, clearing the reachable state of each process in the
-     * list.
+     * A {@link Connection} represents any connection between two processes that can cause a
+     * change in importance in the host process based on the client process and connection state.
      */
-    private static class UnreachedProcessCollector implements Consumer<ProcessRecord> {
-        public ArrayList<ProcessRecord> processList = null;
+    public interface Connection {
+        /**
+         * Compute the impact this connection has on the host's importance values.
+         */
+        void computeHostOomAdjLSP(OomAdjuster oomAdjuster, ProcessRecord host, ProcessRecord client,
+                long now, ProcessRecord topApp, boolean doingAll, int oomAdjReason, int cachedAdj);
+    }
+
+    /**
+     * A helper consumer for marking and collecting reachable processes.
+     */
+    private static class ReachableCollectingConsumer implements
+            BiConsumer<Connection, ProcessRecord> {
+        ArrayList<ProcessRecord> mReachables = null;
+
+        public void init(ArrayList<ProcessRecord> reachables) {
+            mReachables = reachables;
+        }
+
         @Override
-        public void accept(ProcessRecord process) {
-            if (process.mState.isReachable()) {
+        public void accept(Connection unused, ProcessRecord host) {
+            if (host.mState.isReachable()) {
                 return;
             }
-            process.mState.setReachable(true);
-            processList.add(process);
+            host.mState.setReachable(true);
+            mReachables.add(host);
         }
     }
 
-    private final UnreachedProcessCollector mUnreachedProcessCollector =
-            new UnreachedProcessCollector();
+    private final ReachableCollectingConsumer mReachableCollectingConsumer =
+            new ReachableCollectingConsumer();
+
+    /**
+     * A helper consumer for computing the importance of a connection from a client.
+     * Connections for clients marked reachable will be ignored.
+     */
+    private class ComputeConnectionIgnoringReachableClientsConsumer implements
+            BiConsumer<Connection, ProcessRecord> {
+        public OomAdjusterArgs args = null;
+
+        @Override
+        public void accept(Connection conn, ProcessRecord client) {
+            final ProcessRecord host = args.mApp;
+            final ProcessRecord topApp = args.mTopApp;
+            final long now = args.mNow;
+            final @OomAdjReason int oomAdjReason = args.mOomAdjReason;
+
+            if (client.mState.isReachable()) return;
+
+            conn.computeHostOomAdjLSP(OomAdjusterModernImpl.this, host, client, now, topApp, false,
+                    oomAdjReason, UNKNOWN_ADJ);
+        }
+    }
+
+    private final ComputeConnectionIgnoringReachableClientsConsumer
+            mComputeConnectionIgnoringReachableClientsConsumer =
+            new ComputeConnectionIgnoringReachableClientsConsumer();
+
+    /**
+     * A helper consumer for computing host process importance from a connection from a client app.
+     */
+    private class ComputeHostConsumer implements BiConsumer<Connection, ProcessRecord> {
+        public OomAdjusterArgs args = null;
+
+        @Override
+        public void accept(Connection conn, ProcessRecord host) {
+            final ProcessRecord client = args.mApp;
+            final int cachedAdj = args.mCachedAdj;
+            final ProcessRecord topApp = args.mTopApp;
+            final long now = args.mNow;
+            final @OomAdjReason int oomAdjReason = args.mOomAdjReason;
+            final boolean fullUpdate = args.mFullUpdate;
+
+            final int prevProcState = host.mState.getCurProcState();
+            final int prevAdj = host.mState.getCurRawAdj();
+
+            conn.computeHostOomAdjLSP(OomAdjusterModernImpl.this, host, client, now, topApp,
+                    fullUpdate, oomAdjReason, cachedAdj);
+
+            updateProcStateSlotIfNecessary(host, prevProcState);
+            updateAdjSlotIfNecessary(host, prevAdj);
+        }
+    }
+    private final ComputeHostConsumer mComputeHostConsumer = new ComputeHostConsumer();
+
+    /**
+     * A helper consumer for computing all connections from an app.
+     */
+    private class ComputeConnectionsConsumer implements Consumer<OomAdjusterArgs> {
+        @Override
+        public void accept(OomAdjusterArgs args) {
+            final ProcessRecord app = args.mApp;
+            final ActiveUids uids = args.mUids;
+
+            // This process was updated in some way, mark that it was last calculated this sequence.
+            app.mState.setCompletedAdjSeq(mAdjSeq);
+            if (uids != null) {
+                final UidRecord uidRec = app.getUidRecord();
+
+                if (uidRec != null) {
+                    uids.put(uidRec.getUid(), uidRec);
+                }
+            }
+            mComputeHostConsumer.args = args;
+            forEachConnectionLSP(app, mComputeHostConsumer);
+        }
+    }
+    private final ComputeConnectionsConsumer mComputeConnectionsConsumer =
+            new ComputeConnectionsConsumer();
 
     OomAdjusterModernImpl(ActivityManagerService service, ProcessList processList,
             ActiveUids activeUids) {
@@ -617,6 +712,12 @@
         }
     }
 
+    private void updateAdjSlot(ProcessRecord app, int prevRawAdj) {
+        final int slot = adjToSlot(app.mState.getCurRawAdj());
+        final int prevSlot = adjToSlot(prevRawAdj);
+        mProcessRecordAdjNodes.moveAppTo(app, prevSlot, slot);
+    }
+
     private void updateProcStateSlotIfNecessary(ProcessRecord app, int prevProcState) {
         if (app.mState.getCurProcState() != prevProcState) {
             final int slot = processStateToSlot(app.mState.getCurProcState());
@@ -627,359 +728,247 @@
         }
     }
 
+    private void updateProcStateSlot(ProcessRecord app, int prevProcState) {
+        final int slot = processStateToSlot(app.mState.getCurProcState());
+        final int prevSlot = processStateToSlot(prevProcState);
+        mProcessRecordProcStateNodes.moveAppTo(app, prevSlot, slot);
+    }
+
     @Override
-    protected boolean performUpdateOomAdjLSP(ProcessRecord app, @OomAdjReason int oomAdjReason) {
+    protected void performUpdateOomAdjLSP(@OomAdjReason int oomAdjReason) {
         final ProcessRecord topApp = mService.getTopApp();
+        // Clear any pending ones because we are doing a full update now.
+        mPendingProcessSet.clear();
+        mService.mAppProfiler.mHasPreviousProcess = mService.mAppProfiler.mHasHomeProcess = false;
 
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
         mService.mOomAdjProfiler.oomAdjStarted();
-        mAdjSeq++;
 
-        final ProcessStateRecord state = app.mState;
-        final int oldAdj = state.getCurRawAdj();
-        final int cachedAdj = oldAdj >= CACHED_APP_MIN_ADJ
-                ? oldAdj : UNKNOWN_ADJ;
-
-        final ActiveUids uids = mTmpUidRecords;
-        final ArraySet<ProcessRecord> targetProcesses = mTmpProcessSet;
-        final ArrayList<ProcessRecord> reachableProcesses = mTmpProcessList;
-        final long now = SystemClock.uptimeMillis();
-        final long nowElapsed = SystemClock.elapsedRealtime();
-
-        uids.clear();
-        targetProcesses.clear();
-        targetProcesses.add(app);
-        reachableProcesses.clear();
-
-        // Find out all reachable processes from this app.
-        collectReachableProcessesLocked(targetProcesses, reachableProcesses, uids);
-
-        // Copy all of the reachable processes into the target process set.
-        targetProcesses.addAll(reachableProcesses);
-        reachableProcesses.clear();
-
-        final boolean result = performNewUpdateOomAdjLSP(oomAdjReason,
-                topApp, targetProcesses, uids, false, now, cachedAdj);
-
-        reachableProcesses.addAll(targetProcesses);
-        assignCachedAdjIfNecessary(reachableProcesses);
-        for (int  i = uids.size() - 1; i >= 0; i--) {
-            final UidRecord uidRec = uids.valueAt(i);
-            uidRec.forEachProcess(this::updateAppUidRecIfNecessaryLSP);
-        }
-        updateUidsLSP(uids, nowElapsed);
-        for (int i = 0, size = targetProcesses.size(); i < size; i++) {
-            applyOomAdjLSP(targetProcesses.valueAt(i), false, now, nowElapsed, oomAdjReason);
-        }
-        targetProcesses.clear();
-        reachableProcesses.clear();
+        fullUpdateLSP(oomAdjReason);
 
         mService.mOomAdjProfiler.oomAdjEnded();
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-        return result;
     }
 
     @GuardedBy({"mService", "mProcLock"})
     @Override
-    protected void updateOomAdjInnerLSP(@OomAdjReason int oomAdjReason, final ProcessRecord topApp,
-            ArrayList<ProcessRecord> processes, ActiveUids uids, boolean potentialCycles,
-            boolean startProfiling) {
-        final boolean fullUpdate = processes == null;
-        final ArrayList<ProcessRecord> activeProcesses = fullUpdate
-                ? mProcessList.getLruProcessesLOSP() : processes;
-        ActiveUids activeUids = uids;
-        if (activeUids == null) {
-            final int numUids = mActiveUids.size();
-            activeUids = mTmpUidRecords;
-            activeUids.clear();
-            for (int i = 0; i < numUids; i++) {
-                UidRecord uidRec = mActiveUids.valueAt(i);
-                activeUids.put(uidRec.getUid(), uidRec);
-            }
-        }
-
-        if (startProfiling) {
-            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
-            mService.mOomAdjProfiler.oomAdjStarted();
-        }
-        final long now = SystemClock.uptimeMillis();
-        final long nowElapsed = SystemClock.elapsedRealtime();
-        final long oldTime = now - mConstants.mMaxEmptyTimeMillis;
-        final int numProc = activeProcesses.size();
-
-        mAdjSeq++;
-        if (fullUpdate) {
-            mNewNumServiceProcs = 0;
-            mNewNumAServiceProcs = 0;
-        }
-
-        final ArraySet<ProcessRecord> targetProcesses = mTmpProcessSet;
-        targetProcesses.clear();
-        if (!fullUpdate) {
-            targetProcesses.addAll(activeProcesses);
-        }
-
-        performNewUpdateOomAdjLSP(oomAdjReason, topApp, targetProcesses, activeUids,
-                fullUpdate, now, UNKNOWN_ADJ);
-
-        // TODO: b/319163103 - optimize cache adj assignment to not require the whole lru list.
-        assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP());
-        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime);
-        targetProcesses.clear();
-
-        if (startProfiling) {
-            mService.mOomAdjProfiler.oomAdjEnded();
-            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-        }
-        return;
-    }
-
-    /**
-     * Perform the oom adj update on the given {@code targetProcesses}.
-     *
-     * <p>Note: The expectation to the given {@code targetProcesses} is, the caller
-     * must have called {@link collectReachableProcessesLocked} on it.
-     */
-    private boolean performNewUpdateOomAdjLSP(@OomAdjReason int oomAdjReason,
-            ProcessRecord topApp,  ArraySet<ProcessRecord> targetProcesses, ActiveUids uids,
-            boolean fullUpdate, long now, int cachedAdj) {
-
-        final ArrayList<ProcessRecord> clientProcesses = mTmpProcessList2;
-        clientProcesses.clear();
-
-        // We'll need to collect the upstream processes of the target apps here, because those
-        // processes would potentially impact the procstate/adj via bindings.
-        if (!fullUpdate) {
-            collectExcludedClientProcessesLocked(targetProcesses, clientProcesses);
-
-            for (int i = 0, size = targetProcesses.size(); i < size; i++) {
-                final ProcessRecord app = targetProcesses.valueAt(i);
-                app.mState.resetCachedInfo();
-                final UidRecord uidRec = app.getUidRecord();
-                if (uidRec != null) {
-                    if (DEBUG_UID_OBSERVERS) {
-                        Slog.i(TAG_UID_OBSERVERS, "Starting update of " + uidRec);
-                    }
-                    uidRec.reset();
-                }
-            }
-        } else {
-            final ArrayList<ProcessRecord> lru = mProcessList.getLruProcessesLOSP();
-            for (int i = 0, size = lru.size(); i < size; i++) {
-                final ProcessRecord app = lru.get(i);
-                app.mState.resetCachedInfo();
-                final UidRecord uidRec = app.getUidRecord();
-                if (uidRec != null) {
-                    if (DEBUG_UID_OBSERVERS) {
-                        Slog.i(TAG_UID_OBSERVERS, "Starting update of " + uidRec);
-                    }
-                    uidRec.reset();
-                }
-            }
-        }
-
-        updateNewOomAdjInnerLSP(oomAdjReason, topApp, targetProcesses, clientProcesses, uids,
-                cachedAdj, now, fullUpdate);
-
-        clientProcesses.clear();
-
+    protected boolean performUpdateOomAdjLSP(ProcessRecord app, @OomAdjReason int oomAdjReason) {
+        mPendingProcessSet.add(app);
+        performUpdateOomAdjPendingTargetsLocked(oomAdjReason);
         return true;
     }
 
-    /**
-     * Collect the client processes from the given {@code apps}, the result will be returned in the
-     * given {@code clientProcesses}, which will <em>NOT</em> include the processes from the given
-     * {@code apps}.
-     */
     @GuardedBy("mService")
-    private void collectExcludedClientProcessesLocked(ArraySet<ProcessRecord> apps,
-            ArrayList<ProcessRecord> clientProcesses) {
-        // Mark all of the provided apps as reachable to avoid including them in the client list.
-        final int appsSize = apps.size();
-        for (int i = 0; i < appsSize; i++) {
-            final ProcessRecord app = apps.valueAt(i);
-            app.mState.setReachable(true);
-        }
+    @Override
+    protected void performUpdateOomAdjPendingTargetsLocked(@OomAdjReason int oomAdjReason) {
+        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
+        mService.mOomAdjProfiler.oomAdjStarted();
 
-        clientProcesses.clear();
-        mUnreachedProcessCollector.processList = clientProcesses;
-        for (int i = 0; i < appsSize; i++) {
-            final ProcessRecord app = apps.valueAt(i);
-            app.forEachClient(mUnreachedProcessCollector);
+        synchronized (mProcLock) {
+            partialUpdateLSP(oomAdjReason, mPendingProcessSet);
         }
-        mUnreachedProcessCollector.processList = null;
+        mPendingProcessSet.clear();
 
-        // Reset the temporary bits.
-        for (int i = clientProcesses.size() - 1; i >= 0; i--) {
-            clientProcesses.get(i).mState.setReachable(false);
-        }
-        for (int i = 0, size = apps.size(); i < size; i++) {
-            final ProcessRecord app = apps.valueAt(i);
-            app.mState.setReachable(false);
-        }
+        mService.mOomAdjProfiler.oomAdjEnded();
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
     }
 
+    /**
+     * Perform a full update on the entire process list.
+     */
     @GuardedBy({"mService", "mProcLock"})
-    private void updateNewOomAdjInnerLSP(@OomAdjReason int oomAdjReason, final ProcessRecord topApp,
-            ArraySet<ProcessRecord> targetProcesses, ArrayList<ProcessRecord> clientProcesses,
-            ActiveUids uids, int cachedAdj, long now, boolean fullUpdate) {
-        mTmpOomAdjusterArgs.update(topApp, now, cachedAdj, oomAdjReason, uids, fullUpdate);
-
-        mProcessRecordProcStateNodes.resetLastNodes();
-        mProcessRecordAdjNodes.resetLastNodes();
-
-        final int procStateTarget = mProcessRecordProcStateNodes.size() - 1;
-        final int adjTarget = mProcessRecordAdjNodes.size() - 1;
+    private void fullUpdateLSP(@OomAdjReason int oomAdjReason) {
+        final ProcessRecord topApp = mService.getTopApp();
+        final long now = SystemClock.uptimeMillis();
+        final long nowElapsed = SystemClock.elapsedRealtime();
+        final long oldTime = now - mConstants.mMaxEmptyTimeMillis;
 
         mAdjSeq++;
-        // All apps to be updated will be moved to the lowest slot.
-        if (fullUpdate) {
-            // Move all the process record node to the lowest slot, we'll do recomputation on all of
-            // them. Use the processes from the lru list, because the scanning order matters here.
-            final ArrayList<ProcessRecord> lruList = mProcessList.getLruProcessesLOSP();
-            for (int i = procStateTarget; i >= 0; i--) {
-                mProcessRecordProcStateNodes.reset(i);
-                // Force the last node to the head since we'll recompute all of them.
-                mProcessRecordProcStateNodes.setLastNodeToHead(i);
+
+        mNewNumServiceProcs = 0;
+        mNewNumAServiceProcs = 0;
+
+        // Clear the priority queues.
+        mProcessRecordProcStateNodes.reset();
+        mProcessRecordAdjNodes.reset();
+
+        final ArrayList<ProcessRecord> lru = mProcessList.getLruProcessesLOSP();
+        for (int i = lru.size() - 1; i >= 0; i--) {
+            final ProcessRecord app = lru.get(i);
+            final int prevProcState = app.mState.getCurProcState();
+            final int prevAdj = app.mState.getCurRawAdj();
+            app.mState.resetCachedInfo();
+            final UidRecord uidRec = app.getUidRecord();
+            if (uidRec != null) {
+                if (DEBUG_UID_OBSERVERS) {
+                    Slog.i(TAG_UID_OBSERVERS, "Starting update of " + uidRec);
+                }
+                uidRec.reset();
             }
-            // enqueue the targets in the reverse order of the lru list.
-            for (int i = lruList.size() - 1; i >= 0; i--) {
-                mProcessRecordProcStateNodes.append(lruList.get(i), procStateTarget);
-            }
-            // Do the same to the adj nodes.
-            for (int i = adjTarget; i >= 0; i--) {
-                mProcessRecordAdjNodes.reset(i);
-                // Force the last node to the head since we'll recompute all of them.
-                mProcessRecordAdjNodes.setLastNodeToHead(i);
-            }
-            for (int i = lruList.size() - 1; i >= 0; i--) {
-                mProcessRecordAdjNodes.append(lruList.get(i), adjTarget);
-            }
-        } else {
-            // Move the target processes to the lowest slot.
-            for (int i = 0, size = targetProcesses.size(); i < size; i++) {
-                final ProcessRecord app = targetProcesses.valueAt(i);
-                final int procStateSlot = processStateToSlot(app.mState.getCurProcState());
-                final int adjSlot = adjToSlot(app.mState.getCurRawAdj());
-                mProcessRecordProcStateNodes.moveAppTo(app, procStateSlot, procStateTarget);
-                mProcessRecordAdjNodes.moveAppTo(app, adjSlot, adjTarget);
-            }
-            // Move the "lastNode" to head to make sure we scan all nodes in this slot.
-            mProcessRecordProcStateNodes.setLastNodeToHead(procStateTarget);
-            mProcessRecordAdjNodes.setLastNodeToHead(adjTarget);
+
+            // Compute initial values, the procState and adj priority queues will be populated here.
+            computeOomAdjLSP(app, UNKNOWN_ADJ, topApp, true, now, false, false, oomAdjReason,
+                    false);
+            updateProcStateSlot(app, prevProcState);
+            updateAdjSlot(app, prevAdj);
         }
 
-        // All apps to be updated have been moved to the lowest slot.
-        // Do an initial pass of the computation.
-        mProcessRecordProcStateNodes.forEachNewNode(mProcessRecordProcStateNodes.size() - 1,
-                this::computeInitialOomAdjLSP);
-
-        if (!fullUpdate) {
-            // We didn't update the client processes with the computeInitialOomAdjLSP
-            // because they don't need to do so. But they'll be playing vital roles in
-            // computing the bindings. So include them into the scan list below.
-            for (int i = 0, size = clientProcesses.size(); i < size; i++) {
-                mProcessRecordProcStateNodes.moveAppToTail(clientProcesses.get(i));
-            }
-            // We don't update the adj list since we're resetting it below.
-        }
-
-        // Now nodes are set into their slots, without factoring in the bindings.
-        // The nodes between the `lastNode` pointer and the TAIL should be the new nodes.
-        //
-        // The whole rationale here is that, the bindings from client to host app, won't elevate
-        // the host app's procstate/adj higher than the client app's state (BIND_ABOVE_CLIENT
-        // is a special case here, but client app's raw adj is still no less than the host app's).
-        // Therefore, starting from the top to the bottom, for each slot, scan all of the new nodes,
-        // check its bindings, elevate its host app's slot if necessary.
-        //
-        // We'd have to do this in two passes: 1) scan procstate node list; 2) scan adj node list.
-        // Because the procstate and adj are not always in sync - there are cases where
-        // the processes with lower proc state could be getting a higher oom adj score.
-        // And because of this, the procstate and adj node lists are basically two priority heaps.
-        //
-        // As the 2nd pass with the adj node lists potentially includes a significant amount of
-        // duplicated scans as the 1st pass has done, we'll reset the last node pointers for
-        // the adj node list before the 1st pass; so during the 1st pass, if any app's adj slot
-        // gets bumped, we'll only scan those in 2nd pass.
-
+        // Set adj last nodes now, this way a process will only be reevaluated during the adj node
+        // iteration if they adj score changed during the procState node iteration.
         mProcessRecordAdjNodes.resetLastNodes();
+        mTmpOomAdjusterArgs.update(topApp, now, UNKNOWN_ADJ, oomAdjReason, null, true);
+        computeConnectionsLSP();
 
+        assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP());
+        postUpdateOomAdjInnerLSP(oomAdjReason, mActiveUids, now, nowElapsed, oldTime);
+    }
+
+    /**
+     * Traverse the process graph and update processes based on changes in connection importances.
+     */
+    @GuardedBy({"mService", "mProcLock"})
+    private void computeConnectionsLSP() {
         // 1st pass, scan each slot in the procstate node list.
         for (int i = 0, end = mProcessRecordProcStateNodes.size() - 1; i < end; i++) {
-            mProcessRecordProcStateNodes.forEachNewNode(i, this::computeHostOomAdjLSP);
+            mProcessRecordProcStateNodes.forEachNewNode(i, mComputeConnectionsConsumer);
         }
 
         // 2nd pass, scan each slot in the adj node list.
         for (int i = 0, end = mProcessRecordAdjNodes.size() - 1; i < end; i++) {
-            mProcessRecordAdjNodes.forEachNewNode(i, this::computeHostOomAdjLSP);
+            mProcessRecordAdjNodes.forEachNewNode(i, mComputeConnectionsConsumer);
         }
     }
 
-    @GuardedBy({"mService", "mProcLock"})
-    private void computeInitialOomAdjLSP(OomAdjusterArgs args) {
-        final ProcessRecord app = args.mApp;
-        final int cachedAdj = args.mCachedAdj;
-        final ProcessRecord topApp = args.mTopApp;
-        final long now = args.mNow;
-        final int oomAdjReason = args.mOomAdjReason;
-        final ActiveUids uids = args.mUids;
-        final boolean fullUpdate = args.mFullUpdate;
-
-        if (DEBUG_OOM_ADJ) {
-            Slog.i(TAG, "OOM ADJ initial args app=" + app
-                    + " cachedAdj=" + cachedAdj
-                    + " topApp=" + topApp
-                    + " now=" + now
-                    + " oomAdjReason=" + oomAdjReasonToString(oomAdjReason)
-                    + " fullUpdate=" + fullUpdate);
-        }
-
-        if (uids != null) {
-            final UidRecord uidRec = app.getUidRecord();
-
-            if (uidRec != null) {
-                uids.put(uidRec.getUid(), uidRec);
-            }
-        }
-
-        computeOomAdjLSP(app, cachedAdj, topApp, fullUpdate, now, false, false, oomAdjReason,
-                false);
-    }
-
     /**
-     * @return The proposed change to the schedGroup.
+     * Perform a partial update on the target processes and their reachable processes.
      */
     @GuardedBy({"mService", "mProcLock"})
-    @Override
-    protected int setIntermediateAdjLSP(ProcessRecord app, int adj, int prevRawAppAdj,
-            int schedGroup) {
-        schedGroup = super.setIntermediateAdjLSP(app, adj, prevRawAppAdj, schedGroup);
+    private void partialUpdateLSP(@OomAdjReason int oomAdjReason, ArraySet<ProcessRecord> targets) {
+        final ProcessRecord topApp = mService.getTopApp();
+        final long now = SystemClock.uptimeMillis();
+        final long nowElapsed = SystemClock.elapsedRealtime();
+        final long oldTime = now - mConstants.mMaxEmptyTimeMillis;
 
-        updateAdjSlotIfNecessary(app, prevRawAppAdj);
+        ActiveUids activeUids = mTmpUidRecords;
+        activeUids.clear();
+        mTmpOomAdjusterArgs.update(topApp, now, UNKNOWN_ADJ, oomAdjReason, activeUids, false);
 
-        return schedGroup;
+        mAdjSeq++;
+
+        final ArrayList<ProcessRecord> reachables = mTmpProcessList;
+        reachables.clear();
+
+        for (int i = 0, size = targets.size(); i < size; i++) {
+            final ProcessRecord target = targets.valueAtUnchecked(i);
+            target.mState.resetCachedInfo();
+            target.mState.setReachable(true);
+            reachables.add(target);
+        }
+
+        // Collect all processes that are reachable.
+        // Any process not found in this step will not change in importance during this update.
+        collectAndMarkReachableProcessesLSP(reachables);
+
+        // Initialize the reachable processes based on their own values plus any
+        // connections from processes not found in the previous step. Since those non-reachable
+        // processes cannot change as a part of this update, their current values can be used
+        // right now.
+        mProcessRecordProcStateNodes.resetLastNodes();
+        initReachableStatesLSP(reachables, mTmpOomAdjusterArgs);
+
+        // Set adj last nodes now, this way a process will only be reevaluated during the adj node
+        // iteration if they adj score changed during the procState node iteration.
+        mProcessRecordAdjNodes.resetLastNodes();
+        // Now traverse and compute the connections of processes with changed importance.
+        computeConnectionsLSP();
+
+        boolean unassignedAdj = false;
+        for (int i = 0, size = reachables.size(); i < size; i++) {
+            final ProcessStateRecord state = reachables.get(i).mState;
+            state.setReachable(false);
+            state.setCompletedAdjSeq(mAdjSeq);
+            if (state.getCurAdj() >= UNKNOWN_ADJ) {
+                unassignedAdj = true;
+            }
+        }
+
+        // If all processes have an assigned adj, no need to calculate and assign cached adjs.
+        if (unassignedAdj) {
+            // TODO: b/319163103 - optimize cache adj assignment to not require the whole lru list.
+            assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP());
+        }
+
+        // Repopulate any uid record that may have changed.
+        for (int i = 0, size = activeUids.size(); i < size; i++) {
+            final UidRecord ur = activeUids.valueAt(i);
+            ur.reset();
+            for (int j = ur.getNumOfProcs() - 1; j >= 0; j--) {
+                final ProcessRecord proc = ur.getProcessRecordByIndex(j);
+                updateAppUidRecIfNecessaryLSP(proc);
+            }
+        }
+
+        postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime);
     }
 
+    /**
+     * Mark all processes reachable from the {@code reachables} processes and add them to the
+     * provided {@code reachables} list (targets excluded).
+     *
+     * Returns true if a cycle exists within the reachable process graph.
+     */
     @GuardedBy({"mService", "mProcLock"})
-    @Override
-    protected void setIntermediateProcStateLSP(ProcessRecord app, int procState,
-            int prevProcState) {
-        super.setIntermediateProcStateLSP(app, procState, prevProcState);
-
-        updateProcStateSlotIfNecessary(app, prevProcState);
+    private void collectAndMarkReachableProcessesLSP(ArrayList<ProcessRecord> reachables) {
+        mReachableCollectingConsumer.init(reachables);
+        for (int i = 0; i < reachables.size(); i++) {
+            ProcessRecord pr = reachables.get(i);
+            forEachConnectionLSP(pr, mReachableCollectingConsumer);
+        }
     }
 
+    /**
+     * Calculate initial importance states for {@code reachables} and update their slot position
+     * if necessary.
+     */
+    private void initReachableStatesLSP(ArrayList<ProcessRecord> reachables, OomAdjusterArgs args) {
+        for (int i = 0, size = reachables.size(); i < size; i++) {
+            final ProcessRecord reachable = reachables.get(i);
+            final int prevProcState = reachable.mState.getCurProcState();
+            final int prevAdj = reachable.mState.getCurRawAdj();
+
+            args.mApp = reachable;
+            computeOomAdjIgnoringReachablesLSP(args);
+
+            updateProcStateSlot(reachable, prevProcState);
+            updateAdjSlot(reachable, prevAdj);
+        }
+    }
+
+    /**
+     * Calculate initial importance states for {@code app}.
+     * Processes not marked reachable cannot change as a part of this update, so connections from
+     * those process can be calculated now.
+     */
     @GuardedBy({"mService", "mProcLock"})
-    private void computeHostOomAdjLSP(OomAdjusterArgs args) {
+    private void computeOomAdjIgnoringReachablesLSP(OomAdjusterArgs args) {
         final ProcessRecord app = args.mApp;
-        final int cachedAdj = args.mCachedAdj;
         final ProcessRecord topApp = args.mTopApp;
         final long now = args.mNow;
         final @OomAdjReason int oomAdjReason = args.mOomAdjReason;
-        final boolean fullUpdate = args.mFullUpdate;
-        final ActiveUids uids = args.mUids;
 
+        computeOomAdjLSP(app, UNKNOWN_ADJ, topApp, false, now, false, false, oomAdjReason, false);
+
+        mComputeConnectionIgnoringReachableClientsConsumer.args = args;
+        forEachClientConnectionLSP(app, mComputeConnectionIgnoringReachableClientsConsumer);
+    }
+
+    /**
+     * Stream the connections with {@code app} as a client to
+     * {@code connectionConsumer}.
+     */
+    @GuardedBy({"mService", "mProcLock"})
+    private static void forEachConnectionLSP(ProcessRecord app,
+            BiConsumer<Connection, ProcessRecord> connectionConsumer) {
         final ProcessServiceRecord psr = app.mServices;
         for (int i = psr.numberOfConnections() - 1; i >= 0; i--) {
             ConnectionRecord cr = psr.getConnectionAt(i);
@@ -987,16 +976,14 @@
                     ? cr.binding.service.isolationHostProc : cr.binding.service.app;
             if (service == null || service == app
                     || (service.mState.getMaxAdj() >= SYSTEM_ADJ
-                            && service.mState.getMaxAdj() < FOREGROUND_APP_ADJ)
+                    && service.mState.getMaxAdj() < FOREGROUND_APP_ADJ)
                     || (service.mState.getCurAdj() <= FOREGROUND_APP_ADJ
-                            && service.mState.getCurrentSchedulingGroup() > SCHED_GROUP_BACKGROUND
-                            && service.mState.getCurProcState() <= PROCESS_STATE_TOP)
+                    && service.mState.getCurrentSchedulingGroup() > SCHED_GROUP_BACKGROUND
+                    && service.mState.getCurProcState() <= PROCESS_STATE_TOP)
                     || (service.isSdkSandbox && cr.binding.attributedClient != null)) {
                 continue;
             }
-
-            computeServiceHostOomAdjLSP(cr, service, app, now, topApp, fullUpdate, false, false,
-                    oomAdjReason, cachedAdj, false, false);
+            connectionConsumer.accept(cr, service);
         }
 
         for (int i = psr.numberOfSdkSandboxConnections() - 1; i >= 0; i--) {
@@ -1004,15 +991,13 @@
             final ProcessRecord service = cr.binding.service.app;
             if (service == null || service == app
                     || (service.mState.getMaxAdj() >= SYSTEM_ADJ
-                            && service.mState.getMaxAdj() < FOREGROUND_APP_ADJ)
+                    && service.mState.getMaxAdj() < FOREGROUND_APP_ADJ)
                     || (service.mState.getCurAdj() <= FOREGROUND_APP_ADJ
-                            && service.mState.getCurrentSchedulingGroup() > SCHED_GROUP_BACKGROUND
-                            && service.mState.getCurProcState() <= PROCESS_STATE_TOP)) {
+                    && service.mState.getCurrentSchedulingGroup() > SCHED_GROUP_BACKGROUND
+                    && service.mState.getCurProcState() <= PROCESS_STATE_TOP)) {
                 continue;
             }
-
-            computeServiceHostOomAdjLSP(cr, service, app, now, topApp, fullUpdate, false, false,
-                    oomAdjReason, cachedAdj, false, false);
+            connectionConsumer.accept(cr, service);
         }
 
         final ProcessProviderRecord ppr = app.mProviders;
@@ -1021,15 +1006,51 @@
             ProcessRecord provider = cpc.provider.proc;
             if (provider == null || provider == app
                     || (provider.mState.getMaxAdj() >= ProcessList.SYSTEM_ADJ
-                            && provider.mState.getMaxAdj() < FOREGROUND_APP_ADJ)
+                    && provider.mState.getMaxAdj() < FOREGROUND_APP_ADJ)
                     || (provider.mState.getCurAdj() <= FOREGROUND_APP_ADJ
-                            && provider.mState.getCurrentSchedulingGroup() > SCHED_GROUP_BACKGROUND
-                            && provider.mState.getCurProcState() <= PROCESS_STATE_TOP)) {
+                    && provider.mState.getCurrentSchedulingGroup() > SCHED_GROUP_BACKGROUND
+                    && provider.mState.getCurProcState() <= PROCESS_STATE_TOP)) {
                 continue;
             }
+            connectionConsumer.accept(cpc, provider);
+        }
+    }
 
-            computeProviderHostOomAdjLSP(cpc, provider, app, now, topApp, fullUpdate, false, false,
-                    oomAdjReason, cachedAdj, false, false);
+    /**
+     * Stream the connections from clients with {@code app} as the host to {@code
+     * connectionConsumer}.
+     */
+    @GuardedBy({"mService", "mProcLock"})
+    private static void forEachClientConnectionLSP(ProcessRecord app,
+            BiConsumer<Connection, ProcessRecord> connectionConsumer) {
+        final ProcessServiceRecord psr = app.mServices;
+
+        for (int i = psr.numberOfRunningServices() - 1; i >= 0; i--) {
+            final ServiceRecord s = psr.getRunningServiceAt(i);
+            final ArrayMap<IBinder, ArrayList<ConnectionRecord>> serviceConnections =
+                    s.getConnections();
+            for (int j = serviceConnections.size() - 1; j >= 0; j--) {
+                final ArrayList<ConnectionRecord> clist = serviceConnections.valueAt(j);
+                for (int k = clist.size() - 1; k >= 0; k--) {
+                    final ConnectionRecord cr = clist.get(k);
+                    final ProcessRecord client;
+                    if (app.isSdkSandbox && cr.binding.attributedClient != null) {
+                        client = cr.binding.attributedClient;
+                    } else {
+                        client = cr.binding.client;
+                    }
+                    connectionConsumer.accept(cr, client);
+                }
+            }
+        }
+
+        final ProcessProviderRecord ppr = app.mProviders;
+        for (int i = ppr.numberOfProviders() - 1; i >= 0; i--) {
+            final ContentProviderRecord cpr = ppr.getProviderAt(i);
+            for (int j = cpr.connections.size() - 1; j >= 0; j--) {
+                final ContentProviderConnection conn = cpr.connections.get(j);
+                connectionConsumer.accept(conn, conn.client);
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
index 2ef433c..0aa1a69 100644
--- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
@@ -718,9 +718,7 @@
                         mService.mContext, mApp.info.packageName, mApp.info.flags);
             }
         }
-        for (BroadcastQueue queue : mService.mBroadcastQueues) {
-            queue.onApplicationProblemLocked(mApp);
-        }
+        mService.getBroadcastQueue().onApplicationProblemLocked(mApp);
     }
 
     @GuardedBy("mService")
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 89c8994..a1fdd50 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -208,7 +208,7 @@
 
     // Number of levels we have available for different service connection group importance
     // levels.
-    static final int CACHED_APP_IMPORTANCE_LEVELS = 5;
+    public static final int CACHED_APP_IMPORTANCE_LEVELS = 5;
 
     // The B list of SERVICE_ADJ -- these are the old and decrepit
     // services that aren't as shiny and interesting as the ones in the A list.
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 7009bd0..7356588 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -69,10 +69,8 @@
 import com.android.server.wm.WindowProcessListener;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.function.Consumer;
 
 /**
  * Full information about a particular process that
@@ -1659,34 +1657,4 @@
                 && !mOptRecord.shouldNotFreeze()
                 && mState.getCurAdj() >= ProcessList.FREEZER_CUTOFF_ADJ;
     }
-
-    /**
-     * Traverses all client processes and feed them to consumer.
-     */
-    @GuardedBy("mProcLock")
-    void forEachClient(@NonNull Consumer<ProcessRecord> consumer) {
-        for (int i = mServices.numberOfRunningServices() - 1; i >= 0; i--) {
-            final ServiceRecord s = mServices.getRunningServiceAt(i);
-            final ArrayMap<IBinder, ArrayList<ConnectionRecord>> serviceConnections =
-                    s.getConnections();
-            for (int j = serviceConnections.size() - 1; j >= 0; j--) {
-                final ArrayList<ConnectionRecord> clist = serviceConnections.valueAt(j);
-                for (int k = clist.size() - 1; k >= 0; k--) {
-                    final ConnectionRecord cr = clist.get(k);
-                    if (isSdkSandbox && cr.binding.attributedClient != null) {
-                        consumer.accept(cr.binding.attributedClient);
-                    } else {
-                        consumer.accept(cr.binding.client);
-                    }
-                }
-            }
-        }
-        for (int i = mProviders.numberOfProviders() - 1; i >= 0; i--) {
-            final ContentProviderRecord cpr = mProviders.getProviderAt(i);
-            for (int j = cpr.connections.size() - 1; j >= 0; j--) {
-                final ContentProviderConnection conn = cpr.connections.get(j);
-                consumer.accept(conn.client);
-            }
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/am/ProcessServiceRecord.java b/services/core/java/com/android/server/am/ProcessServiceRecord.java
index 562beaf..3d695bc 100644
--- a/services/core/java/com/android/server/am/ProcessServiceRecord.java
+++ b/services/core/java/com/android/server/am/ProcessServiceRecord.java
@@ -541,7 +541,7 @@
     private void removeSdkSandboxConnectionIfNecessary(ConnectionRecord connection) {
         final ProcessRecord attributedClient = connection.binding.attributedClient;
         if (attributedClient != null && connection.binding.service.isSdkSandbox) {
-            if (attributedClient.mServices.mSdkSandboxConnections == null) {
+            if (attributedClient.mServices.mSdkSandboxConnections != null) {
                 attributedClient.mServices.mSdkSandboxConnections.remove(connection);
             }
         }
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 3abfe082..13a1807 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1419,7 +1419,8 @@
 
     private boolean allowBiometricUnlockForPrivateProfile() {
         return android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace();
+                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures();
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index d584c99..d4e46a9 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -179,15 +179,18 @@
             nextConsumer.consume(current);
         } else if (mNextConsumer != null) {
             mNextConsumer.add(nextConsumer);
-        } else {
+        } else if (mLightSensor != null) {
             mDestroyed = false;
             mNextConsumer = nextConsumer;
             enableLightSensorLoggingLocked();
+        } else {
+            Slog.w(TAG, "No light sensor - use current to consume");
+            nextConsumer.consume(current);
         }
     }
 
     private void enableLightSensorLoggingLocked() {
-        if (!mEnabled) {
+        if (!mEnabled && mLightSensor != null) {
             mEnabled = true;
             mLastAmbientLux = -1;
             mSensorManager.registerListener(mLightSensorListener, mLightSensor,
@@ -201,7 +204,7 @@
     private void disableLightSensorLoggingLocked(boolean destroying) {
         resetTimerLocked(false /* start */);
 
-        if (mEnabled) {
+        if (mEnabled && mLightSensor != null) {
             mEnabled = false;
             if (!destroying) {
                 mLastAmbientLux = -1;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 6b8586a..7ee2a7a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -119,7 +119,7 @@
      * Receives the incoming binder calls from FaceManager.
      */
     @VisibleForTesting final class FaceServiceWrapper extends IFaceService.Stub {
-        @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) {
@@ -149,11 +149,7 @@
             return proto.getBytes();
         }
 
-        @android.annotation.EnforcePermission(
-                anyOf = {
-                        android.Manifest.permission.USE_BIOMETRIC_INTERNAL,
-                        android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION
-                })
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(
                 String opPackageName) {
@@ -297,29 +293,6 @@
                     restricted, statsClient, isKeyguard);
         }
 
-        @android.annotation.EnforcePermission(
-                android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION)
-        @Override // Binder call
-        public long authenticateInBackground(final IBinder token, final long operationId,
-                final IFaceServiceReceiver receiver, final FaceAuthenticateOptions options) {
-            // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or
-            //  lockdown, something wrong happened. See similar path in FingerprintService.
-
-            super.authenticateInBackground_enforcePermission();
-
-            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
-            if (provider == null) {
-                Slog.w(TAG, "Null provider for authenticate");
-                return -1;
-            }
-            options.setSensorId(provider.first);
-
-            return provider.second.scheduleAuthenticate(token, operationId,
-                    0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options,
-                    false /* restricted */, BiometricsProtoEnums.CLIENT_UNKNOWN /* statsClient */,
-                    true /* allowBackgroundAuthentication */);
-        }
-
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public long detectFace(final IBinder token,
@@ -583,11 +556,7 @@
             return provider.getEnrolledFaces(sensorId, userId);
         }
 
-        @android.annotation.EnforcePermission(
-                anyOf = {
-                        android.Manifest.permission.USE_BIOMETRIC_INTERNAL,
-                        android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION
-                })
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) {
             super.hasEnrolledFaces_enforcePermission();
diff --git a/services/core/java/com/android/server/clipboard/ArcClipboardMonitor.java b/services/core/java/com/android/server/clipboard/ArcClipboardMonitor.java
new file mode 100644
index 0000000..413020e
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/ArcClipboardMonitor.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.clipboard;
+
+import android.annotation.Nullable;
+import android.content.ClipData;
+
+import com.android.server.LocalServices;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class ArcClipboardMonitor implements Consumer<ClipData> {
+    private static final String TAG = "ArcClipboardMonitor";
+
+    public interface ArcClipboardBridge {
+        /**
+         * Called when a clipboard content is updated.
+         */
+        void onPrimaryClipChanged(ClipData data);
+
+        /**
+         * Passes the callback to set a new clipboard content with a uid.
+         */
+        void setHandler(BiConsumer<ClipData, Integer> setAndroidClipboard);
+    }
+
+    private ArcClipboardBridge mBridge;
+    private BiConsumer<ClipData, Integer> mAndroidClipboardSetter;
+
+    ArcClipboardMonitor(final BiConsumer<ClipData, Integer> setAndroidClipboard) {
+        mAndroidClipboardSetter = setAndroidClipboard;
+        LocalServices.addService(ArcClipboardMonitor.class, this);
+    }
+
+    @Override
+    public void accept(final @Nullable ClipData clip) {
+        if (mBridge != null) {
+            mBridge.onPrimaryClipChanged(clip);
+        }
+    }
+
+    /**
+     * Sets the other end of the clipboard bridge.
+     */
+    public void setClipboardBridge(ArcClipboardBridge bridge) {
+        mBridge = bridge;
+        mBridge.setHandler(mAndroidClipboardSetter);
+    }
+}
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 49f6070..4c3020f 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -151,7 +151,7 @@
     private final ContentCaptureManagerInternal mContentCaptureInternal;
     private final AutofillManagerInternal mAutofillInternal;
     private final IBinder mPermissionOwner;
-    private final Consumer<ClipData> mEmulatorClipboardMonitor;
+    private final Consumer<ClipData> mClipboardMonitor;
     private final Handler mWorkerHandler;
 
     @GuardedBy("mLock")
@@ -192,7 +192,7 @@
         final IBinder permOwner = mUgmInternal.newUriPermissionOwner("clipboard");
         mPermissionOwner = permOwner;
         if (Build.IS_EMULATOR) {
-            mEmulatorClipboardMonitor = new EmulatorClipboardMonitor((clip) -> {
+            mClipboardMonitor = new EmulatorClipboardMonitor((clip) -> {
                 synchronized (mLock) {
                     Clipboard clipboard = getClipboardLocked(0, DEVICE_ID_DEFAULT);
                     if (clipboard != null) {
@@ -201,8 +201,12 @@
                     }
                 }
             });
+        } else if (Build.IS_ARC) {
+            mClipboardMonitor = new ArcClipboardMonitor((clip, uid) -> {
+                setPrimaryClipInternal(clip, uid);
+            });
         } else {
-            mEmulatorClipboardMonitor = (clip) -> {};
+            mClipboardMonitor = (clip) -> {};
         }
 
         updateConfig();
@@ -937,7 +941,7 @@
     private void setPrimaryClipInternalLocked(
             @Nullable ClipData clip, int uid, int deviceId, @Nullable String sourcePackage) {
         if (deviceId == DEVICE_ID_DEFAULT) {
-            mEmulatorClipboardMonitor.accept(clip);
+            mClipboardMonitor.accept(clip);
         }
 
         final int userId = UserHandle.getUserId(uid);
diff --git a/services/core/java/com/android/server/connectivity/TEST_MAPPING b/services/core/java/com/android/server/connectivity/TEST_MAPPING
index f508319..55601bc 100644
--- a/services/core/java/com/android/server/connectivity/TEST_MAPPING
+++ b/services/core/java/com/android/server/connectivity/TEST_MAPPING
@@ -8,6 +8,19 @@
               }
           ],
           "file_patterns": ["VpnIkeV2Utils\\.java", "VpnProfileStore\\.java"]
+      },
+      {
+          "name":"FrameworksVpnTests",
+          "options": [
+              {
+                "exclude-annotation": "com.android.testutils.SkipPresubmit"
+              }
+          ],
+          "file_patterns":[
+              "Vpn\\.java",
+              "VpnIkeV2Utils\\.java",
+              "VpnProfileStore\\.java"
+          ]
       }
   ],
   "presubmit-large": [
@@ -26,10 +39,5 @@
           ],
         "file_patterns": ["Vpn\\.java", "VpnIkeV2Utils\\.java", "VpnProfileStore\\.java"]
       }
-  ],
-  "postsubmit":[
-    {
-      "name":"FrameworksVpnTests"
-    }
   ]
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index a43f93a..91e560e 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -20,7 +20,6 @@
 
 import android.annotation.Nullable;
 import android.hardware.display.DisplayManagerInternal;
-import android.os.PowerManager;
 import android.os.Trace;
 
 /**
@@ -110,7 +109,6 @@
         try {
             mDisplayOffloader.stopOffload();
             mIsActive = false;
-            mDisplayPowerController.setBrightnessFromOffload(PowerManager.BRIGHTNESS_INVALID_FLOAT);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 2010aca..7f014f6 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -375,7 +375,8 @@
     // information.
     // At the time of this writing, this value is changed within updatePowerState() only, which is
     // limited to the thread used by DisplayControllerHandler.
-    private final BrightnessReason mBrightnessReason = new BrightnessReason();
+    @VisibleForTesting
+    final BrightnessReason mBrightnessReason = new BrightnessReason();
     private final BrightnessReason mBrightnessReasonTemp = new BrightnessReason();
 
     // Brightness animation ramp rates in brightness units per second
@@ -1379,7 +1380,7 @@
         // Switch to doze auto-brightness mode if needed
         if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null
                 && !mAutomaticBrightnessController.isInIdleMode()) {
-            mAutomaticBrightnessController.switchMode(mPowerRequest.policy == POLICY_DOZE
+            mAutomaticBrightnessController.switchMode(Display.isDozeState(state)
                     ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
         }
 
@@ -1434,7 +1435,9 @@
         float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
         // Apply auto-brightness.
         int brightnessAdjustmentFlags = 0;
-        if (Float.isNaN(brightnessState)) {
+        // AutomaticBrightnessStrategy has higher priority than OffloadBrightnessStrategy
+        if (Float.isNaN(brightnessState)
+                || mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_OFFLOAD) {
             if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
                 brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
                         mTempBrightnessEvent);
@@ -1455,8 +1458,11 @@
                     if (mScreenOffBrightnessSensorController != null) {
                         mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
                     }
+                    setBrightnessFromOffload(PowerManager.BRIGHTNESS_INVALID_FLOAT);
                 } else {
                     mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
+                    // Restore the lower-priority brightness strategy
+                    brightnessState = displayBrightnessState.getBrightness();
                 }
             }
         } else {
@@ -3020,9 +3026,10 @@
                     setDwbcLoggingEnabled(msg.arg1);
                     break;
                 case MSG_SET_BRIGHTNESS_FROM_OFFLOAD:
-                    mDisplayBrightnessController.setBrightnessFromOffload(
-                            Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
+                    if (mDisplayBrightnessController.setBrightnessFromOffload(
+                            Float.intBitsToFloat(msg.arg1))) {
+                        updatePowerState();
+                    }
                     break;
             }
         }
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index f6d02db..34d53be 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -26,6 +26,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.AutomaticBrightnessController;
 import com.android.server.display.BrightnessMappingStrategy;
 import com.android.server.display.BrightnessSetting;
@@ -175,14 +176,19 @@
 
     /**
      * Sets the brightness from the offload session.
+     * @return Whether the offload brightness has changed
      */
-    public void setBrightnessFromOffload(float brightness) {
+    public boolean setBrightnessFromOffload(float brightness) {
         synchronized (mLock) {
-            if (mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() != null) {
+            if (mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() != null
+                    && !BrightnessSynchronizer.floatEquals(mDisplayBrightnessStrategySelector
+                    .getOffloadBrightnessStrategy().getOffloadScreenBrightness(), brightness)) {
                 mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()
                         .setOffloadScreenBrightness(brightness);
+                return true;
             }
         }
+        return false;
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
index 8e84450..71cc872 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
@@ -133,7 +133,8 @@
         } else if (BrightnessUtils.isValidBrightnessValue(
                 mTemporaryBrightnessStrategy.getTemporaryScreenBrightness())) {
             displayBrightnessStrategy = mTemporaryBrightnessStrategy;
-        } else if (mOffloadBrightnessStrategy != null && BrightnessUtils.isValidBrightnessValue(
+        } else if (mAutomaticBrightnessStrategy.shouldUseAutoBrightness()
+                && mOffloadBrightnessStrategy != null && BrightnessUtils.isValidBrightnessValue(
                 mOffloadBrightnessStrategy.getOffloadScreenBrightness())) {
             displayBrightnessStrategy = mOffloadBrightnessStrategy;
         }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index d1ca49b..8b54b22 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -108,7 +108,6 @@
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
                 && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
-                && brightnessReason != BrightnessReason.REASON_OFFLOAD
                 && mAutomaticBrightnessController != null;
         mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
                 && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index a23c08a..d876a38 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -198,7 +198,7 @@
         addValidationInfo(Constants.MESSAGE_CEC_VERSION,
                 oneByteValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT);
         addValidationInfo(Constants.MESSAGE_SET_MENU_LANGUAGE,
-                new AsciiValidator(3), ADDR_NOT_UNREGISTERED, ADDR_BROADCAST);
+                new AsciiValidator(3), ADDR_TV, ADDR_BROADCAST);
 
         ParameterValidator statusRequestValidator = new MinimumOneByteRangeValidator(0x01, 0x03);
         addValidationInfo(Constants.MESSAGE_DECK_CONTROL,
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index c8c66238..d0532b99 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4665,15 +4665,27 @@
         public void onAudioDeviceVolumeChanged(
                 @NonNull AudioDeviceAttributes audioDevice,
                 @NonNull VolumeInfo volumeInfo) {
+            int localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress();
 
-            // Do nothing if the System Audio device does not support <Set Audio Volume Level>
+            // We can't send <Set Audio Volume Level> if the System Audio device doesn't support it.
+            // But AudioService has already updated its volume and expects us to set it.
+            // So the best we can do is to send <Give Audio Status>, which triggers
+            // <Report Audio Status>, which should update AudioService with its correct volume.
             if (mSystemAudioDevice.getDeviceFeatures().getSetAudioVolumeLevelSupport()
                     != DeviceFeatures.FEATURE_SUPPORTED) {
+                // Update the volume tracked in AbsoluteVolumeAudioStatusAction
+                // so it correctly processes the next <Report Audio Status>
+                HdmiCecLocalDevice avbDevice = isTvDevice() ? tv() : playback();
+                avbDevice.updateAvbVolume(volumeInfo.getVolumeIndex());
+                // Send <Give Audio Status>
+                sendCecCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
+                        localDeviceAddress,
+                        mSystemAudioDevice.getLogicalAddress()
+                ));
                 return;
             }
 
             // Send <Set Audio Volume Level> to notify the System Audio device of the volume change
-            int localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress();
             sendCecCommand(SetAudioVolumeLevelMessage.build(
                             localDeviceAddress,
                             mSystemAudioDevice.getLogicalAddress(),
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index a79e771..05b1cb69 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -54,6 +54,7 @@
 import android.hardware.input.InputSensorInfo;
 import android.hardware.input.InputSettings;
 import android.hardware.input.KeyboardLayout;
+import android.hardware.input.KeyboardLayoutSelectionResult;
 import android.hardware.input.TouchCalibration;
 import android.hardware.lights.Light;
 import android.hardware.lights.LightState;
@@ -1244,9 +1245,9 @@
     }
 
     @Override // Binder call
-    public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
-            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
-            @Nullable InputMethodSubtype imeSubtype) {
+    public KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice(
+            InputDeviceIdentifier identifier, @UserIdInt int userId,
+            @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) {
         return mKeyboardLayoutManager.getKeyboardLayoutForInputDevice(identifier, userId,
                 imeInfo, imeSubtype);
     }
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 46668de..283e692 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -16,10 +16,11 @@
 
 package com.android.server.input;
 
-import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEFAULT;
-import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE;
-import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER;
-import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD;
+import static android.hardware.input.KeyboardLayoutSelectionResult.FAILED;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT;
 
 import android.annotation.AnyThread;
 import android.annotation.MainThread;
@@ -46,6 +47,7 @@
 import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.InputManager;
 import android.hardware.input.KeyboardLayout;
+import android.hardware.input.KeyboardLayoutSelectionResult;
 import android.icu.lang.UScript;
 import android.icu.util.ULocale;
 import android.os.Bundle;
@@ -79,7 +81,6 @@
 import com.android.server.LocalServices;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent;
-import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 
 import libcore.io.Streams;
@@ -130,7 +131,8 @@
     // This cache stores "best-matched" layouts so that we don't need to run the matching
     // algorithm repeatedly.
     @GuardedBy("mKeyboardLayoutCache")
-    private final Map<String, KeyboardLayoutInfo> mKeyboardLayoutCache = new ArrayMap<>();
+    private final Map<String, KeyboardLayoutSelectionResult> mKeyboardLayoutCache =
+            new ArrayMap<>();
     private final Object mImeInfoLock = new Object();
     @Nullable
     @GuardedBy("mImeInfoLock")
@@ -222,17 +224,17 @@
         } else {
             Set<String> selectedLayouts = new HashSet<>();
             List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping();
-            List<KeyboardLayoutInfo> layoutInfoList = new ArrayList<>();
+            List<KeyboardLayoutSelectionResult> resultList = new ArrayList<>();
             boolean hasMissingLayout = false;
             for (ImeInfo imeInfo : imeInfoList) {
                 // Check if the layout has been previously configured
-                KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(
+                KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal(
                         keyboardIdentifier, imeInfo);
-                boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null;
+                boolean noLayoutFound = result.getLayoutDescriptor() == null;
                 if (!noLayoutFound) {
-                    selectedLayouts.add(layoutInfo.mDescriptor);
+                    selectedLayouts.add(result.getLayoutDescriptor());
                 }
-                layoutInfoList.add(layoutInfo);
+                resultList.add(result);
                 hasMissingLayout |= noLayoutFound;
             }
 
@@ -260,7 +262,7 @@
                     }
 
                     if (shouldLogConfiguration) {
-                        logKeyboardConfigurationEvent(inputDevice, imeInfoList, layoutInfoList,
+                        logKeyboardConfigurationEvent(inputDevice, imeInfoList, resultList,
                                 !mDataStore.hasInputDeviceEntry(key));
                     }
                 } finally {
@@ -757,10 +759,10 @@
         String keyboardLayoutDescriptor;
         if (useNewSettingsUi()) {
             synchronized (mImeInfoLock) {
-                KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(
+                KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal(
                         new KeyboardIdentifier(identifier, languageTag, layoutType),
                         mCurrentImeInfo);
-                keyboardLayoutDescriptor = layoutInfo == null ? null : layoutInfo.mDescriptor;
+                keyboardLayoutDescriptor = result.getLayoutDescriptor();
             }
         } else {
             keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
@@ -788,26 +790,26 @@
     }
 
     @AnyThread
-    @Nullable
-    public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
-            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
-            @Nullable InputMethodSubtype imeSubtype) {
+    @NonNull
+    public KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice(
+            InputDeviceIdentifier identifier, @UserIdInt int userId,
+            @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) {
         if (!useNewSettingsUi()) {
             Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported");
-            return null;
+            return FAILED;
         }
         InputDevice inputDevice = getInputDevice(identifier);
         if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
-            return null;
+            return FAILED;
         }
         KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice);
-        KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(
+        KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal(
                 keyboardIdentifier, new ImeInfo(userId, imeInfo, imeSubtype));
         if (DEBUG) {
             Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : "
-                    + userId + ", subtype = " + imeSubtype + " -> " + layoutInfo);
+                    + userId + ", subtype = " + imeSubtype + " -> " + result);
         }
-        return layoutInfo != null ? layoutInfo.mDescriptor : null;
+        return result;
     }
 
     @AnyThread
@@ -942,13 +944,13 @@
     }
 
     @Nullable
-    private KeyboardLayoutInfo getKeyboardLayoutForInputDeviceInternal(
+    private KeyboardLayoutSelectionResult getKeyboardLayoutForInputDeviceInternal(
             KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) {
         String layoutKey = new LayoutKey(keyboardIdentifier, imeInfo).toString();
         synchronized (mDataStore) {
             String layout = mDataStore.getKeyboardLayout(keyboardIdentifier.toString(), layoutKey);
             if (layout != null) {
-                return new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER);
+                return new KeyboardLayoutSelectionResult(layout, LAYOUT_SELECTION_CRITERIA_USER);
             }
         }
 
@@ -961,17 +963,17 @@
                 KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
                         keyboardIdentifier, imeInfo);
                 // Call auto-matching algorithm to find the best matching layout
-                KeyboardLayoutInfo layoutInfo =
+                KeyboardLayoutSelectionResult result =
                         getDefaultKeyboardLayoutBasedOnImeInfo(keyboardIdentifier, imeInfo,
                                 layoutList);
-                mKeyboardLayoutCache.put(layoutKey, layoutInfo);
-                return layoutInfo;
+                mKeyboardLayoutCache.put(layoutKey, result);
+                return result;
             }
         }
     }
 
-    @Nullable
-    private static KeyboardLayoutInfo getDefaultKeyboardLayoutBasedOnImeInfo(
+    @NonNull
+    private static KeyboardLayoutSelectionResult getDefaultKeyboardLayoutBasedOnImeInfo(
             KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo,
             KeyboardLayout[] layoutList) {
         Arrays.sort(layoutList);
@@ -986,7 +988,7 @@
                                     + "vendor and product Ids. " + keyboardIdentifier
                                     + " : " + layout.getDescriptor());
                 }
-                return new KeyboardLayoutInfo(layout.getDescriptor(),
+                return new KeyboardLayoutSelectionResult(layout.getDescriptor(),
                         LAYOUT_SELECTION_CRITERIA_DEVICE);
             }
         }
@@ -1004,13 +1006,14 @@
                                     + "HW information (Language tag and Layout type). "
                                     + keyboardIdentifier + " : " + layoutDesc);
                 }
-                return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_DEVICE);
+                return new KeyboardLayoutSelectionResult(layoutDesc,
+                        LAYOUT_SELECTION_CRITERIA_DEVICE);
             }
         }
 
         if (imeInfo == null || imeInfo.mImeSubtypeHandle == null || imeInfo.mImeSubtype == null) {
             // Can't auto select layout based on IME info is null
-            return null;
+            return FAILED;
         }
 
         InputMethodSubtype subtype = imeInfo.mImeSubtype;
@@ -1027,9 +1030,10 @@
                             + layoutDesc);
         }
         if (layoutDesc != null) {
-            return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD);
+            return new KeyboardLayoutSelectionResult(layoutDesc,
+                    LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD);
         }
-        return null;
+        return FAILED;
     }
 
     @Nullable
@@ -1246,21 +1250,23 @@
     }
 
     private void logKeyboardConfigurationEvent(@NonNull InputDevice inputDevice,
-            @NonNull List<ImeInfo> imeInfoList, @NonNull List<KeyboardLayoutInfo> layoutInfoList,
+            @NonNull List<ImeInfo> imeInfoList,
+            @NonNull List<KeyboardLayoutSelectionResult> resultList,
             boolean isFirstConfiguration) {
-        if (imeInfoList.isEmpty() || layoutInfoList.isEmpty()) {
+        if (imeInfoList.isEmpty() || resultList.isEmpty()) {
             return;
         }
         KeyboardConfigurationEvent.Builder configurationEventBuilder =
                 new KeyboardConfigurationEvent.Builder(inputDevice).setIsFirstTimeConfiguration(
                         isFirstConfiguration);
         for (int i = 0; i < imeInfoList.size(); i++) {
-            KeyboardLayoutInfo layoutInfo = layoutInfoList.get(i);
+            KeyboardLayoutSelectionResult result = resultList.get(i);
             String layoutName = null;
             int layoutSelectionCriteria = LAYOUT_SELECTION_CRITERIA_DEFAULT;
-            if (layoutInfo != null && layoutInfo.mDescriptor != null) {
-                layoutSelectionCriteria = layoutInfo.mSelectionCriteria;
-                KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(layoutInfo.mDescriptor);
+            if (result != null && result.getLayoutDescriptor() != null) {
+                layoutSelectionCriteria = result.getSelectionCriteria();
+                KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(
+                        result.getLayoutDescriptor());
                 if (d != null) {
                     layoutName = d.keyboardLayoutName;
                 }
@@ -1477,33 +1483,6 @@
         }
     }
 
-    private static class KeyboardLayoutInfo {
-        @Nullable
-        private final String mDescriptor;
-        @LayoutSelectionCriteria
-        private final int mSelectionCriteria;
-
-        private KeyboardLayoutInfo(@Nullable String descriptor,
-                @LayoutSelectionCriteria int selectionCriteria) {
-            mDescriptor = descriptor;
-            mSelectionCriteria = selectionCriteria;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj instanceof KeyboardLayoutInfo) {
-                return Objects.equals(mDescriptor, ((KeyboardLayoutInfo) obj).mDescriptor)
-                        && mSelectionCriteria == ((KeyboardLayoutInfo) obj).mSelectionCriteria;
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return 31 * mSelectionCriteria + mDescriptor.hashCode();
-        }
-    }
-
     private interface KeyboardLayoutVisitor {
         void visitKeyboardLayout(Resources resources,
                 int keyboardLayoutResId, KeyboardLayout layout);
diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
index f53b941..b8ae737 100644
--- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
+++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
@@ -16,14 +16,18 @@
 
 package com.android.server.input;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD;
+import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT;
+import static android.hardware.input.KeyboardLayoutSelectionResult.layoutSelectionCriteriaToString;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.role.RoleManager;
 import android.content.Intent;
 import android.hardware.input.KeyboardLayout;
+import android.hardware.input.KeyboardLayoutSelectionResult.LayoutSelectionCriteria;
 import android.icu.util.ULocale;
 import android.text.TextUtils;
 import android.util.Log;
@@ -40,7 +44,6 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.policy.ModifierShortcutManager;
 
-import java.lang.annotation.Retention;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -57,32 +60,6 @@
     // (requires restart)
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    @Retention(SOURCE)
-    @IntDef(prefix = {"LAYOUT_SELECTION_CRITERIA_"}, value = {
-            LAYOUT_SELECTION_CRITERIA_UNSPECIFIED,
-            LAYOUT_SELECTION_CRITERIA_USER,
-            LAYOUT_SELECTION_CRITERIA_DEVICE,
-            LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
-            LAYOUT_SELECTION_CRITERIA_DEFAULT
-    })
-    public @interface LayoutSelectionCriteria {
-    }
-
-    /** Unspecified layout selection criteria */
-    public static final int LAYOUT_SELECTION_CRITERIA_UNSPECIFIED = 0;
-
-    /** Manual selection by user */
-    public static final int LAYOUT_SELECTION_CRITERIA_USER = 1;
-
-    /** Auto-detection based on device provided language tag and layout type */
-    public static final int LAYOUT_SELECTION_CRITERIA_DEVICE = 2;
-
-    /** Auto-detection based on IME provided language tag and layout type */
-    public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 3;
-
-    /** Default selection */
-    public static final int LAYOUT_SELECTION_CRITERIA_DEFAULT = 4;
-
     @VisibleForTesting
     static final String DEFAULT_LAYOUT_NAME = "Default";
 
@@ -629,30 +606,16 @@
 
         @Override
         public String toString() {
-            return "{keyboardLanguageTag = " + keyboardLanguageTag + " keyboardLayoutType = "
+            return "{keyboardLanguageTag = " + keyboardLanguageTag
+                    + " keyboardLayoutType = "
                     + KeyboardLayout.LayoutType.getLayoutNameFromValue(keyboardLayoutType)
-                    + " keyboardLayoutName = " + keyboardLayoutName + " layoutSelectionCriteria = "
-                    + getStringForSelectionCriteria(layoutSelectionCriteria)
-                    + "imeLanguageTag = " + imeLanguageTag + " imeLayoutType = "
-                    + KeyboardLayout.LayoutType.getLayoutNameFromValue(imeLayoutType) + "}";
-        }
-    }
-
-    private static String getStringForSelectionCriteria(
-            @LayoutSelectionCriteria int layoutSelectionCriteria) {
-        switch (layoutSelectionCriteria) {
-            case LAYOUT_SELECTION_CRITERIA_UNSPECIFIED:
-                return "LAYOUT_SELECTION_CRITERIA_UNSPECIFIED";
-            case LAYOUT_SELECTION_CRITERIA_USER:
-                return "LAYOUT_SELECTION_CRITERIA_USER";
-            case LAYOUT_SELECTION_CRITERIA_DEVICE:
-                return "LAYOUT_SELECTION_CRITERIA_DEVICE";
-            case LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD:
-                return "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD";
-            case LAYOUT_SELECTION_CRITERIA_DEFAULT:
-                return "LAYOUT_SELECTION_CRITERIA_DEFAULT";
-            default:
-                return "INVALID_CRITERIA";
+                    + " keyboardLayoutName = " + keyboardLayoutName
+                    + " layoutSelectionCriteria = "
+                    + layoutSelectionCriteriaToString(layoutSelectionCriteria)
+                    + " imeLanguageTag = " + imeLanguageTag
+                    + " imeLayoutType = " + KeyboardLayout.LayoutType.getLayoutNameFromValue(
+                    imeLayoutType)
+                    + "}";
         }
     }
 
diff --git a/services/core/java/com/android/server/input/debug/FocusEventDebugView.java b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java
index b30f5ec..6eae9a4 100644
--- a/services/core/java/com/android/server/input/debug/FocusEventDebugView.java
+++ b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java
@@ -229,7 +229,8 @@
     /** Report a key event to the debug view. */
     @AnyThread
     public void reportKeyEvent(KeyEvent event) {
-        post(() -> handleKeyEvent(KeyEvent.obtain((KeyEvent) event)));
+        KeyEvent keyEvent = KeyEvent.obtain(event);
+        post(() -> handleKeyEvent(keyEvent));
     }
 
     /** Report a motion event to the debug view. */
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index fee0342..2205986 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -39,7 +39,6 @@
 import static android.server.inputmethod.InputMethodManagerServiceProto.IN_FULLSCREEN_MODE;
 import static android.server.inputmethod.InputMethodManagerServiceProto.IS_INTERACTIVE;
 import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_IME_TARGET_WINDOW_NAME;
-import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_SWITCH_USER_ID;
 import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_IME_WITH_HARD_KEYBOARD;
 import static android.server.inputmethod.InputMethodManagerServiceProto.SYSTEM_READY;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -276,9 +275,6 @@
     @NonNull
     private final String[] mNonPreemptibleInputMethods;
 
-    @UserIdInt
-    private int mLastSwitchUserId;
-
     final Context mContext;
     final Resources mRes;
     private final Handler mHandler;
@@ -1311,20 +1307,35 @@
 
         @Override
         public void onPackageDataCleared(String packageName, int uid) {
+            final int userId = getChangingUserId();
             synchronized (ImfLock.class) {
+                final boolean isCurrentUser = (userId == mSettings.getUserId());
+                final AdditionalSubtypeMap additionalSubtypeMap;
+                final InputMethodSettings settings;
+                if (isCurrentUser) {
+                    additionalSubtypeMap = mAdditionalSubtypeMap;
+                    settings = mSettings;
+                } else {
+                    additionalSubtypeMap = AdditionalSubtypeUtils.load(userId);
+                    settings = queryInputMethodServicesInternal(mContext, userId,
+                            additionalSubtypeMap, DirectBootAwareness.AUTO);
+                }
+
                 // Note that one package may implement multiple IMEs.
                 final ArrayList<String> changedImes = new ArrayList<>();
-                for (InputMethodInfo imi : mSettings.getMethodList()) {
+                for (InputMethodInfo imi : settings.getMethodList()) {
                     if (imi.getPackageName().equals(packageName)) {
                         changedImes.add(imi.getId());
                     }
                 }
                 final AdditionalSubtypeMap newMap =
-                        mAdditionalSubtypeMap.cloneWithRemoveOrSelf(changedImes);
-                if (newMap != mAdditionalSubtypeMap) {
-                    mAdditionalSubtypeMap = newMap;
+                        additionalSubtypeMap.cloneWithRemoveOrSelf(changedImes);
+                if (newMap != additionalSubtypeMap) {
+                    if (isCurrentUser) {
+                        mAdditionalSubtypeMap = newMap;
+                    }
                     AdditionalSubtypeUtils.save(
-                            mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId());
+                            newMap, settings.getMethodMap(), settings.getUserId());
                 }
                 if (!changedImes.isEmpty()) {
                     mChangedPackages.add(packageName);
@@ -1649,8 +1660,6 @@
 
         final int userId = mActivityManagerInternal.getCurrentUserId();
 
-        mLastSwitchUserId = userId;
-
         // mSettings should be created before buildInputMethodListLocked
         mSettings = InputMethodSettings.createEmptyMap(userId);
 
@@ -1837,7 +1846,6 @@
                     + " selectedIme=" + mSettings.getSelectedInputMethod());
         }
 
-        mLastSwitchUserId = newUserId;
         if (mIsInteractive && clientToBeReset != null) {
             final ClientState cs = mClientController.getClient(clientToBeReset.asBinder());
             if (cs == null) {
@@ -4710,7 +4718,6 @@
             proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked()));
             proto.write(CUR_TOKEN_DISPLAY_ID, mCurTokenDisplayId);
             proto.write(SYSTEM_READY, mSystemReady);
-            proto.write(LAST_SWITCH_USER_ID, mLastSwitchUserId);
             proto.write(HAVE_CONNECTION, hasConnectionLocked());
             proto.write(BOUND_TO_METHOD, mBoundToMethod);
             proto.write(IS_INTERACTIVE, mIsInteractive);
@@ -6288,8 +6295,6 @@
         @ShellCommandResult
         private int onCommandWithSystemIdentity(@Nullable String cmd) {
             switch (TextUtils.emptyIfNull(cmd)) {
-                case "get-last-switch-user-id":
-                    return mService.getLastSwitchUserId(this);
                 case "tracing":
                     return mService.handleShellCommandTraceInputMethod(this);
                 case "ime": {  // For "adb shell ime <command>".
@@ -6403,15 +6408,6 @@
     // ----------------------------------------------------------------------
     // Shell command handlers:
 
-    @BinderThread
-    @ShellCommandResult
-    private int getLastSwitchUserId(@NonNull ShellCommand shellCommand) {
-        synchronized (ImfLock.class) {
-            shellCommand.getOutPrintWriter().println(mLastSwitchUserId);
-            return ShellCommandResult.SUCCESS;
-        }
-    }
-
     /**
      * Handles {@code adb shell ime list}.
      * @param shellCommand {@link ShellCommand} object that is handling this command.
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index fc99471..0ca4808 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -449,7 +449,7 @@
     }
 
     /**
-     * Sends a reliable message rom this client to a nanoapp.
+     * Sends a reliable message from this client to a nanoapp.
      *
      * @param message the message to send
      * @param transactionCallback The callback to use to confirm the delivery of the message for
@@ -473,6 +473,12 @@
             @Nullable IContextHubTransactionCallback transactionCallback) {
         ContextHubServiceUtil.checkPermissions(mContext);
 
+        // Clear the isReliable and messageSequenceNumber fields.
+        // These will be set to true and a real value if the message
+        // is reliable.
+        message.setIsReliable(false);
+        message.setMessageSequenceNumber(0);
+
         @ContextHubTransaction.Result int result;
         if (isRegistered()) {
             int authState = mMessageChannelNanoappIdMap.getOrDefault(
@@ -485,7 +491,9 @@
                 // Return a bland error code for apps targeting old SDKs since they wouldn't be able
                 // to use an error code added in S.
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
-            } else if (authState == AUTHORIZATION_UNKNOWN) {
+            }
+
+            if (authState == AUTHORIZATION_UNKNOWN) {
                 // Only check permissions the first time a nanoapp is queried since nanoapp
                 // permissions don't currently change at runtime. If the host permission changes
                 // later, that'll be checked by onOpChanged.
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 29ea071..c80f988 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -814,7 +814,8 @@
         // storage is locked, instead of when the user is stopped.  This would ensure the flags get
         // reset if CE storage is locked later for a user that allows delayed locking.
         if (android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()) {
+                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
             UserProperties userProperties = mUserManager.getUserProperties(UserHandle.of(userId));
             if (userProperties != null && userProperties.getAllowStoppingUserWithDelayedLocking()) {
                 return;
diff --git a/services/core/java/com/android/server/net/TEST_MAPPING b/services/core/java/com/android/server/net/TEST_MAPPING
index e0376ed..4fc1a17 100644
--- a/services/core/java/com/android/server/net/TEST_MAPPING
+++ b/services/core/java/com/android/server/net/TEST_MAPPING
@@ -27,6 +27,15 @@
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
+    },
+    {
+      "name": "FrameworksVpnTests",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        }
+      ],
+      "file_patterns": ["VpnManagerService\\.java"]
     }
   ]
 }
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index f645eaa..3ecc58e 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -942,6 +942,23 @@
         return false;
     }
 
+    protected boolean isPackageOrComponentAllowedWithPermission(ComponentName component,
+            int userId) {
+        if (!(isPackageOrComponentAllowed(component.flattenToString(), userId)
+                || isPackageOrComponentAllowed(component.getPackageName(), userId))) {
+            return false;
+        }
+        return componentHasBindPermission(component, userId);
+    }
+
+    private boolean componentHasBindPermission(ComponentName component, int userId) {
+        ServiceInfo info = getServiceInfo(component, userId);
+        if (info == null) {
+            return false;
+        }
+        return mConfig.bindPermission.equals(info.permission);
+    }
+
     boolean isPackageOrComponentUserSet(String pkgOrComponent, int userId) {
         synchronized (mApproved) {
             ArraySet<String> services = mUserSetServices.get(userId);
@@ -1003,6 +1020,7 @@
                     for (int uid : uidList) {
                         if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) {
                             anyServicesInvolved = true;
+                            trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid));
                         }
                     }
                 }
@@ -1135,8 +1153,7 @@
 
         synchronized (mMutex) {
             if (enabled) {
-                if (isPackageOrComponentAllowed(component.flattenToString(), userId)
-                        || isPackageOrComponentAllowed(component.getPackageName(), userId)) {
+                if (isPackageOrComponentAllowedWithPermission(component, userId)) {
                     registerServiceLocked(component, userId);
                 } else {
                     Slog.d(TAG, component + " no longer has permission to be bound");
@@ -1270,6 +1287,33 @@
         return removed;
     }
 
+    private void trimApprovedListsForInvalidServices(String packageName, int userId) {
+        synchronized (mApproved) {
+            final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(userId);
+            if (approvedByType == null) {
+                return;
+            }
+            for (int i = 0; i < approvedByType.size(); i++) {
+                final ArraySet<String> approved = approvedByType.valueAt(i);
+                for (int j = approved.size() - 1; j >= 0; j--) {
+                    final String approvedPackageOrComponent = approved.valueAt(j);
+                    if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) {
+                        final ComponentName component = ComponentName.unflattenFromString(
+                                approvedPackageOrComponent);
+                        if (component != null && !componentHasBindPermission(component, userId)) {
+                            approved.removeAt(j);
+                            if (DEBUG) {
+                                Slog.v(TAG, "Removing " + approvedPackageOrComponent
+                                        + " from approved list; no bind permission found "
+                                        + mConfig.bindPermission);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     protected String getPackageName(String packageOrComponent) {
         final ComponentName component = ComponentName.unflattenFromString(packageOrComponent);
         if (component != null) {
@@ -1519,8 +1563,7 @@
     void reregisterService(final ComponentName cn, final int userId) {
         // If rebinding a package that died, ensure it still has permission
         // after the rebind delay
-        if (isPackageOrComponentAllowed(cn.getPackageName(), userId)
-                || isPackageOrComponentAllowed(cn.flattenToString(), userId)) {
+        if (isPackageOrComponentAllowedWithPermission(cn, userId)) {
             registerService(cn, userId);
         }
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 3a7ac0b..b98424c 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1204,6 +1204,10 @@
         }
     }
 
+    private static boolean privateSpaceFlagsEnabled() {
+        return allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures();
+    }
+
     private final class SavePolicyFileRunnable implements Runnable {
         @Override
         public void run() {
@@ -2142,7 +2146,7 @@
         }
 
         private boolean isProfileUnavailable(String action) {
-            return allowPrivateProfile() ?
+            return privateSpaceFlagsEnabled() ?
                     action.equals(Intent.ACTION_PROFILE_UNAVAILABLE) :
                     action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
         }
@@ -2744,7 +2748,7 @@
         filter.addAction(Intent.ACTION_USER_REMOVED);
         filter.addAction(Intent.ACTION_USER_UNLOCKED);
         filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
-        if (allowPrivateProfile()){
+        if (privateSpaceFlagsEnabled()){
             filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE);
         }
         getContext().registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null);
@@ -12050,11 +12054,12 @@
         @Override
         public void onServiceAdded(ManagedServiceInfo info) {
             if (lifetimeExtensionRefactor()) {
-                // Generally, only System or System UI should have the permissions to call
-                // registerSystemService.
-                // isCallerSystemorPhone tells us whether the caller is System. Then, if it's not
-                // the system, we know it's system UI.
-                info.isSystemUi = !isCallerSystemOrPhone();
+                // We explicitly check the status bar permission for the uid in the info object.
+                // We can't use the calling uid here because it's probably always system server.
+                // Note that this will also be true for the shell.
+                info.isSystemUi = getContext().checkPermission(
+                        android.Manifest.permission.STATUS_BAR_SERVICE, -1, info.uid)
+                        == PERMISSION_GRANTED;
             }
             final INotificationListener listener = (INotificationListener) info.service;
             final NotificationRankingUpdate update;
diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java
deleted file mode 100644
index 37b263c..0000000
--- a/services/core/java/com/android/server/notification/ZenAdapters.java
+++ /dev/null
@@ -1,83 +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.server.notification;
-
-import android.app.Flags;
-import android.app.NotificationManager.Policy;
-import android.service.notification.ZenModeConfig;
-import android.service.notification.ZenPolicy;
-
-/**
- * Converters between different Zen representations.
- */
-class ZenAdapters {
-
-    static ZenPolicy notificationPolicyToZenPolicy(Policy policy) {
-        ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder()
-                .allowAlarms(policy.allowAlarms())
-                .allowCalls(
-                        policy.allowCalls()
-                                ? ZenModeConfig.getZenPolicySenders(policy.allowCallsFrom())
-                                : ZenPolicy.PEOPLE_TYPE_NONE)
-                .allowConversations(
-                        policy.allowConversations()
-                                ? notificationPolicyConversationSendersToZenPolicy(
-                                        policy.allowConversationsFrom())
-                                : ZenPolicy.CONVERSATION_SENDERS_NONE)
-                .allowEvents(policy.allowEvents())
-                .allowMedia(policy.allowMedia())
-                .allowMessages(
-                        policy.allowMessages()
-                                ? ZenModeConfig.getZenPolicySenders(policy.allowMessagesFrom())
-                                : ZenPolicy.PEOPLE_TYPE_NONE)
-                .allowReminders(policy.allowReminders())
-                .allowRepeatCallers(policy.allowRepeatCallers())
-                .allowSystem(policy.allowSystem());
-
-        if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
-            zenPolicyBuilder.showBadges(policy.showBadges())
-                    .showFullScreenIntent(policy.showFullScreenIntents())
-                    .showInAmbientDisplay(policy.showAmbient())
-                    .showInNotificationList(policy.showInNotificationList())
-                    .showLights(policy.showLights())
-                    .showPeeking(policy.showPeeking())
-                    .showStatusBarIcons(policy.showStatusBarIcons());
-        }
-
-        if (Flags.modesApi()) {
-            zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels());
-        }
-
-        return zenPolicyBuilder.build();
-    }
-
-    @ZenPolicy.ConversationSenders
-    private static int notificationPolicyConversationSendersToZenPolicy(
-            int npPriorityConversationSenders) {
-        switch (npPriorityConversationSenders) {
-            case Policy.CONVERSATION_SENDERS_ANYONE:
-                return ZenPolicy.CONVERSATION_SENDERS_ANYONE;
-            case Policy.CONVERSATION_SENDERS_IMPORTANT:
-                return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
-            case Policy.CONVERSATION_SENDERS_NONE:
-                return ZenPolicy.CONVERSATION_SENDERS_NONE;
-            case Policy.CONVERSATION_SENDERS_UNSET:
-            default:
-                return ZenPolicy.CONVERSATION_SENDERS_UNSET;
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index b9a267f..8e37b4f 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -32,6 +32,7 @@
 import android.content.pm.PackageManager;
 import android.os.Process;
 import android.service.notification.DNDPolicyProto;
+import android.service.notification.ZenAdapters;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
 import android.service.notification.ZenModeConfig.ZenRule;
@@ -591,9 +592,11 @@
                 // This applies to both call and message senders, but not conversation senders,
                 // where they use the same enum values.
                 proto.write(DNDPolicyProto.ALLOW_CALLS_FROM,
-                        ZenModeConfig.getZenPolicySenders(mNewPolicy.allowCallsFrom()));
+                        ZenAdapters.notificationPolicySendersToZenPolicyPeopleType(
+                                mNewPolicy.allowCallsFrom()));
                 proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM,
-                        ZenModeConfig.getZenPolicySenders(mNewPolicy.allowMessagesFrom()));
+                        ZenAdapters.notificationPolicySendersToZenPolicyPeopleType(
+                                mNewPolicy.allowMessagesFrom()));
                 proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM,
                         mNewPolicy.allowConversationsFrom());
 
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 6857869..289faf4 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -35,6 +35,8 @@
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 import static com.android.internal.util.Preconditions.checkArgument;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.DrawableRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -84,6 +86,7 @@
 import android.service.notification.Condition;
 import android.service.notification.ConditionProviderService;
 import android.service.notification.DeviceEffectsApplier;
+import android.service.notification.ZenAdapters;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
@@ -535,36 +538,40 @@
     public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule,
             @ConfigChangeOrigin int origin, String reason, int callingUid) {
         requirePublicOrigin("updateAutomaticZenRule", origin);
-        ZenModeConfig newConfig;
+        if (ruleId == null) {
+            throw new IllegalArgumentException("ruleId cannot be null");
+        }
         synchronized (mConfigLock) {
             if (mConfig == null) return false;
             if (DEBUG) {
                 Log.d(TAG, "updateAutomaticZenRule zenRule=" + automaticZenRule
                         + " reason=" + reason);
             }
-            newConfig = mConfig.copy();
-            ZenModeConfig.ZenRule rule;
-            if (ruleId == null) {
-                throw new IllegalArgumentException("Rule doesn't exist");
-            } else {
-                rule = newConfig.automaticRules.get(ruleId);
-                if (rule == null || !canManageAutomaticZenRule(rule)) {
-                    throw new SecurityException(
-                            "Cannot update rules not owned by your condition provider");
-                }
+            ZenModeConfig.ZenRule oldRule = mConfig.automaticRules.get(ruleId);
+            if (oldRule == null || !canManageAutomaticZenRule(oldRule)) {
+                throw new SecurityException(
+                        "Cannot update rules not owned by your condition provider");
             }
+            ZenModeConfig newConfig = mConfig.copy();
+            ZenModeConfig.ZenRule newRule = requireNonNull(newConfig.automaticRules.get(ruleId));
             if (!Flags.modesApi()) {
-                if (rule.enabled != automaticZenRule.isEnabled()) {
-                    dispatchOnAutomaticRuleStatusChanged(mConfig.user, rule.getPkg(), ruleId,
+                if (newRule.enabled != automaticZenRule.isEnabled()) {
+                    dispatchOnAutomaticRuleStatusChanged(mConfig.user, newRule.getPkg(), ruleId,
                             automaticZenRule.isEnabled()
                                     ? AUTOMATIC_RULE_STATUS_ENABLED
                                     : AUTOMATIC_RULE_STATUS_DISABLED);
                 }
             }
 
-            populateZenRule(rule.pkg, automaticZenRule, rule, origin, /* isNew= */ false);
+            boolean updated = populateZenRule(newRule.pkg, automaticZenRule, newRule,
+                    origin, /* isNew= */ false);
+            if (Flags.modesApi() && !updated) {
+                // Bail out so we don't have the side effects of updating a rule (i.e. dropping
+                // condition) when no changes happen.
+                return true;
+            }
             return setConfigLocked(newConfig, origin, reason,
-                    rule.component, true, callingUid);
+                    newRule.component, true, callingUid);
         }
     }
 
@@ -1072,31 +1079,67 @@
         return null;
     }
 
-    private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
+    /**
+     * Populates a {@code ZenRule} with the content of the {@link AutomaticZenRule}. Can be used for
+     * both rule creation or update (distinguished by the {@code isNew} parameter. The change is
+     * applied differently depending on the origin; for example app-provided changes might be
+     * ignored (if the rule was previously customized by the user), while user-provided changes
+     * update the user-modified bitmasks for any modifications.
+     *
+     * <p>Returns {@code true} if the rule was modified. Note that this is not equivalent to
+     * {@link ZenRule#equals} or {@link AutomaticZenRule#equals}, for various reasons:
+     * <ul>
+     *     <li>some metadata-related fields are not considered
+     *     <li>some fields (like {@code condition} are always reset, and ignored for this result
+     *     <li>an app may provide changes that are not actually applied, as described above
+     * </ul>
+     */
+    private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenRule rule,
                          @ConfigChangeOrigin int origin, boolean isNew) {
         if (Flags.modesApi()) {
+            boolean modified = false;
             // These values can always be edited by the app, so we apply changes immediately.
             if (isNew) {
                 rule.id = ZenModeConfig.newRuleId();
                 rule.creationTime = mClock.millis();
-                rule.component = automaticZenRule.getOwner();
+                rule.component = azr.getOwner();
                 rule.pkg = pkg;
+                modified = true;
             }
 
             rule.condition = null;
-            rule.conditionId = automaticZenRule.getConditionId();
-            if (rule.enabled != automaticZenRule.isEnabled()) {
-                rule.snoozing = false;
+            if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
+                rule.conditionId = azr.getConditionId();
+                modified = true;
             }
-            rule.enabled = automaticZenRule.isEnabled();
-            rule.configurationActivity = automaticZenRule.getConfigurationActivity();
-            rule.allowManualInvocation = automaticZenRule.isManualInvocationAllowed();
-            rule.iconResName =
-                    drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId());
-            rule.triggerDescription = automaticZenRule.getTriggerDescription();
-            rule.type = automaticZenRule.getType();
+            if (rule.enabled != azr.isEnabled()) {
+                rule.enabled = azr.isEnabled();
+                rule.snoozing = false;
+                modified = true;
+            }
+            if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) {
+                rule.configurationActivity = azr.getConfigurationActivity();
+                modified = true;
+            }
+            if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) {
+                rule.allowManualInvocation = azr.isManualInvocationAllowed();
+                modified = true;
+            }
+            String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId());
+            if (!Objects.equals(rule.iconResName, iconResName)) {
+                rule.iconResName = iconResName;
+                modified = true;
+            }
+            if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) {
+                rule.triggerDescription = azr.getTriggerDescription();
+                modified = true;
+            }
+            if (rule.type != azr.getType()) {
+                rule.type = azr.getType();
+                modified = true;
+            }
             // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined.
-            rule.modified = automaticZenRule.isModified();
+            rule.modified = azr.isModified();
 
             // Name is treated differently than other values:
             // App is allowed to update name if the name was not modified by the user (even if
@@ -1106,7 +1149,8 @@
             String previousName = rule.name;
             if (isNew || doesOriginAlwaysUpdateValues(origin)
                     || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) {
-                rule.name = automaticZenRule.getName();
+                rule.name = azr.getName();
+                modified |= !Objects.equals(rule.name, previousName);
             }
 
             // For the remaining values, rules can always have all values updated if:
@@ -1118,50 +1162,56 @@
 
             // For all other values, if updates are not allowed, we discard the update.
             if (!updateValues) {
-                return;
+                return modified;
             }
 
             // Updates the bitmasks if the origin of the change is the user.
             boolean updateBitmask = (origin == UPDATE_ORIGIN_USER);
 
-            if (updateBitmask && !TextUtils.equals(previousName, automaticZenRule.getName())) {
+            if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) {
                 rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
             }
             int newZenMode = NotificationManager.zenModeFromInterruptionFilter(
-                    automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
-            if (updateBitmask && rule.zenMode != newZenMode) {
-                rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+                    azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+            if (rule.zenMode != newZenMode) {
+                rule.zenMode = newZenMode;
+                if (updateBitmask) {
+                    rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+                }
+                modified = true;
             }
 
-            // Updates the values in the ZenRule itself.
-            rule.zenMode = newZenMode;
-
             // Updates the bitmask and values for all policy fields, based on the origin.
-            updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask, isNew);
+            modified |= updatePolicy(rule, azr.getZenPolicy(), updateBitmask, isNew);
 
             // Updates the bitmask and values for all device effect fields, based on the origin.
-            updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(),
+            modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(),
                     origin == UPDATE_ORIGIN_APP, updateBitmask);
+
+            return modified;
         } else {
-            if (rule.enabled != automaticZenRule.isEnabled()) {
+            if (rule.enabled != azr.isEnabled()) {
                 rule.snoozing = false;
             }
-            rule.name = automaticZenRule.getName();
+            rule.name = azr.getName();
             rule.condition = null;
-            rule.conditionId = automaticZenRule.getConditionId();
-            rule.enabled = automaticZenRule.isEnabled();
-            rule.modified = automaticZenRule.isModified();
-            rule.zenPolicy = automaticZenRule.getZenPolicy();
+            rule.conditionId = azr.getConditionId();
+            rule.enabled = azr.isEnabled();
+            rule.modified = azr.isModified();
+            rule.zenPolicy = azr.getZenPolicy();
             rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
-                    automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
-            rule.configurationActivity = automaticZenRule.getConfigurationActivity();
+                    azr.getInterruptionFilter(), Global.ZEN_MODE_OFF);
+            rule.configurationActivity = azr.getConfigurationActivity();
 
             if (isNew) {
                 rule.id = ZenModeConfig.newRuleId();
                 rule.creationTime = System.currentTimeMillis();
-                rule.component = automaticZenRule.getOwner();
+                rule.component = azr.getOwner();
                 rule.pkg = pkg;
             }
+
+            // Only the MODES_API path cares about the result, so just return whatever here.
+            return true;
         }
     }
 
@@ -1181,16 +1231,19 @@
      * provided {@code ZenRule}, keeping any pre-existing settings from {@code zenRule.zenPolicy}
      * for any unset policy fields in {@code newPolicy}. The user-modified bitmask is updated to
      * reflect the changes being applied (if applicable, i.e. if the update is from the user).
+     *
+     * <p>Returns {@code true} if the policy of the rule was modified.
      */
-    private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
+    private boolean updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
             boolean updateBitmask, boolean isNew) {
         if (newPolicy == null) {
             if (isNew) {
                 // Newly created rule with no provided policy; fill in with the default.
                 zenRule.zenPolicy = mDefaultConfig.toZenPolicy();
+                return true;
             }
             // Otherwise, a null policy means no policy changes, so we can stop here.
-            return;
+            return false;
         }
 
         // If oldPolicy is null, we compare against the default policy when determining which
@@ -1271,6 +1324,8 @@
             }
             zenRule.zenPolicyUserModifiedFields = userModifiedFields;
         }
+
+        return !newPolicy.equals(oldPolicy);
     }
 
     /**
@@ -1282,12 +1337,14 @@
      * <p>Apps cannot turn on hidden effects (those tagged as {@code @hide}), so those fields are
      * treated especially: for a new rule, they are blanked out; for an updated rule, previous
      * values are preserved.
+     *
+     * <p>Returns {@code true} if the device effects of the rule were modified.
      */
-    private static void updateZenDeviceEffects(ZenRule zenRule,
+    private static boolean updateZenDeviceEffects(ZenRule zenRule,
             @Nullable ZenDeviceEffects newEffects, boolean isFromApp, boolean updateBitmask) {
         // Same as with ZenPolicy, supplying null effects means keeping the previous ones.
         if (newEffects == null) {
-            return;
+            return false;
         }
 
         ZenDeviceEffects oldEffects = zenRule.zenDeviceEffects != null
@@ -1348,6 +1405,8 @@
             }
             zenRule.zenDeviceEffectsUserModifiedFields = userModifiedFields;
         }
+
+        return !newEffects.equals(oldEffects);
     }
 
     private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
@@ -2504,7 +2563,7 @@
         if (resId == 0) {
             return null;
         }
-        Objects.requireNonNull(packageName);
+        requireNonNull(packageName);
         try {
             final Resources res = mPm.getResourcesForApplication(packageName);
             return res.getResourceName(resId);
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 0abe50f..71800ef 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -32,12 +32,13 @@
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.app.ondeviceintelligence.IResponseCallback;
 import android.app.ondeviceintelligence.IStreamingResponseCallback;
-import android.app.ondeviceintelligence.ITokenCountCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
 import android.app.ondeviceintelligence.OnDeviceIntelligenceManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.ICancellationSignal;
 import android.os.ParcelFileDescriptor;
@@ -47,11 +48,12 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.service.ondeviceintelligence.IOnDeviceIntelligenceService;
-import android.service.ondeviceintelligence.IOnDeviceTrustedInferenceService;
+import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService;
 import android.service.ondeviceintelligence.IRemoteProcessingService;
 import android.service.ondeviceintelligence.IRemoteStorageService;
 import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
 import android.service.ondeviceintelligence.OnDeviceIntelligenceService;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -69,7 +71,7 @@
  * This is the system service for handling calls on the
  * {@link android.app.ondeviceintelligence.OnDeviceIntelligenceManager}. This
  * service holds connection references to the underlying remote services i.e. the isolated service
- * {@link  OnDeviceTrustedInferenceService} and a regular
+ * {@link OnDeviceSandboxedInferenceService} and a regular
  * service counter part {@link OnDeviceIntelligenceService}.
  *
  * Note: Both the remote services run under the SYSTEM user, as we cannot have separate instance of
@@ -90,7 +92,7 @@
     protected final Object mLock = new Object();
 
 
-    private RemoteOnDeviceTrustedInferenceService mRemoteInferenceService;
+    private RemoteOnDeviceSandboxedInferenceService mRemoteInferenceService;
     private RemoteOnDeviceIntelligenceService mRemoteOnDeviceIntelligenceService;
     volatile boolean mIsServiceEnabled;
 
@@ -165,7 +167,7 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.getFeature(id, featureCallback));
+                    service -> service.getFeature(Binder.getCallingUid(), id, featureCallback));
         }
 
         @Override
@@ -185,7 +187,7 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.listFeatures(listFeaturesCallback));
+                    service -> service.listFeatures(Binder.getCallingUid(), listFeaturesCallback));
         }
 
         @Override
@@ -207,7 +209,8 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.getFeatureDetails(feature, featureDetailsCallback));
+                    service -> service.getFeatureDetails(Binder.getCallingUid(), feature,
+                            featureDetailsCallback));
         }
 
         @Override
@@ -227,33 +230,35 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.requestFeatureDownload(feature, cancellationSignal,
+                    service -> service.requestFeatureDownload(Binder.getCallingUid(), feature,
+                            cancellationSignal,
                             downloadCallback));
         }
 
 
         @Override
-        public void requestTokenCount(Feature feature,
+        public void requestTokenInfo(Feature feature,
                 Content request, ICancellationSignal cancellationSignal,
-                ITokenCountCallback tokenCountcallback) throws RemoteException {
+                ITokenInfoCallback tokenInfoCallback) throws RemoteException {
             Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing");
             Objects.requireNonNull(feature);
             Objects.requireNonNull(request);
-            Objects.requireNonNull(tokenCountcallback);
+            Objects.requireNonNull(tokenInfoCallback);
 
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
             if (!mIsServiceEnabled) {
                 Slog.w(TAG, "Service not available");
-                tokenCountcallback.onFailure(
+                tokenInfoCallback.onFailure(
                         OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE,
                         "OnDeviceIntelligenceManagerService is unavailable",
                         new PersistableBundle());
             }
-            ensureRemoteTrustedInferenceServiceInitialized();
+            ensureRemoteInferenceServiceInitialized();
             mRemoteInferenceService.post(
-                    service -> service.requestTokenCount(feature, request, cancellationSignal,
-                            tokenCountcallback));
+                    service -> service.requestTokenInfo(Binder.getCallingUid(), feature, request,
+                            cancellationSignal,
+                            tokenInfoCallback));
         }
 
         @Override
@@ -267,7 +272,6 @@
             Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest");
             Objects.requireNonNull(feature);
             Objects.requireNonNull(responseCallback);
-            Objects.requireNonNull(request);
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
             if (!mIsServiceEnabled) {
@@ -277,9 +281,10 @@
                         "OnDeviceIntelligenceManagerService is unavailable",
                         new PersistableBundle());
             }
-            ensureRemoteTrustedInferenceServiceInitialized();
+            ensureRemoteInferenceServiceInitialized();
             mRemoteInferenceService.post(
-                    service -> service.processRequest(feature, request, requestType,
+                    service -> service.processRequest(Binder.getCallingUid(), feature, request,
+                            requestType,
                             cancellationSignal, processingSignal,
                             responseCallback));
         }
@@ -293,7 +298,6 @@
                 IStreamingResponseCallback streamingCallback) throws RemoteException {
             Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming");
             Objects.requireNonNull(feature);
-            Objects.requireNonNull(request);
             Objects.requireNonNull(streamingCallback);
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
@@ -304,9 +308,10 @@
                         "OnDeviceIntelligenceManagerService is unavailable",
                         new PersistableBundle());
             }
-            ensureRemoteTrustedInferenceServiceInitialized();
+            ensureRemoteInferenceServiceInitialized();
             mRemoteInferenceService.post(
-                    service -> service.processRequestStreaming(feature, request, requestType,
+                    service -> service.processRequestStreaming(Binder.getCallingUid(), feature,
+                            request, requestType,
                             cancellationSignal, processingSignal,
                             streamingCallback));
         }
@@ -346,7 +351,7 @@
                     Bundle processingState,
                     IProcessingUpdateStatusCallback callback) {
                 try {
-                    ensureRemoteTrustedInferenceServiceInitialized();
+                    ensureRemoteInferenceServiceInitialized();
                     mRemoteInferenceService.post(
                             service -> service.updateProcessingState(
                                     processingState, callback));
@@ -363,22 +368,24 @@
         };
     }
 
-    private void ensureRemoteTrustedInferenceServiceInitialized() throws RemoteException {
+    private void ensureRemoteInferenceServiceInitialized() throws RemoteException {
         synchronized (mLock) {
             if (mRemoteInferenceService == null) {
                 String serviceName = mContext.getResources().getString(
-                        R.string.config_defaultOnDeviceTrustedInferenceService);
+                        R.string.config_defaultOnDeviceSandboxedInferenceService);
                 validateService(serviceName, true);
-                mRemoteInferenceService = new RemoteOnDeviceTrustedInferenceService(mContext,
+                mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext,
                         ComponentName.unflattenFromString(serviceName),
                         UserHandle.SYSTEM.getIdentifier());
                 mRemoteInferenceService.setServiceLifecycleCallbacks(
                         new ServiceConnector.ServiceLifecycleCallbacks<>() {
                             @Override
                             public void onConnected(
-                                    @NonNull IOnDeviceTrustedInferenceService service) {
+                                    @NonNull IOnDeviceSandboxedInferenceService service) {
                                 try {
                                     ensureRemoteIntelligenceServiceInitialized();
+                                    mRemoteOnDeviceIntelligenceService.post(
+                                            intelligenceService -> intelligenceService.notifyInferenceServiceConnected());
                                     service.registerRemoteStorageService(
                                             getIRemoteStorageService());
                                 } catch (RemoteException ex) {
@@ -433,7 +440,7 @@
             }
 
             checkServiceRequiresPermission(serviceInfo,
-                    Manifest.permission.BIND_ON_DEVICE_TRUSTED_SERVICE);
+                    Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
             if (!isIsolatedService(serviceInfo)) {
                 throw new SecurityException(
                         "Call required an isolated service, but the configured service: "
diff --git a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceTrustedInferenceService.java b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
similarity index 73%
rename from services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceTrustedInferenceService.java
rename to services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
index cc8e788..69ba1d2 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceTrustedInferenceService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
@@ -22,18 +22,18 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.service.ondeviceintelligence.IOnDeviceTrustedInferenceService;
-import android.service.ondeviceintelligence.OnDeviceTrustedInferenceService;
+import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
 
 import com.android.internal.infra.ServiceConnector;
 
 
 /**
- * Manages the connection to the remote on-device trusted inference service. Also, handles unbinding
+ * Manages the connection to the remote on-device sand boxed inference service. Also, handles unbinding
  * logic set by the service implementation via a SecureSettings flag.
  */
-public class RemoteOnDeviceTrustedInferenceService extends
-        ServiceConnector.Impl<IOnDeviceTrustedInferenceService> {
+public class RemoteOnDeviceSandboxedInferenceService extends
+        ServiceConnector.Impl<IOnDeviceSandboxedInferenceService> {
     /**
      * Creates an instance of {@link ServiceConnector}
      *
@@ -43,12 +43,12 @@
      *                {@link Context#unbindService unbinding}
      * @param userId  to be used for {@link Context#bindServiceAsUser binding}
      */
-    RemoteOnDeviceTrustedInferenceService(Context context, ComponentName serviceName,
+    RemoteOnDeviceSandboxedInferenceService(Context context, ComponentName serviceName,
             int userId) {
         super(context, new Intent(
-                        OnDeviceTrustedInferenceService.SERVICE_INTERFACE).setComponent(serviceName),
+                        OnDeviceSandboxedInferenceService.SERVICE_INTERFACE).setComponent(serviceName),
                 BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId,
-                IOnDeviceTrustedInferenceService.Stub::asInterface);
+                IOnDeviceSandboxedInferenceService.Stub::asInterface);
 
         // Bind right away
         connect();
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 4f86adf..4eb8b2b 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -354,6 +354,10 @@
         DevicePolicyManager getDevicePolicyManager() {
             return mContext.getSystemService(DevicePolicyManager.class);
         }
+
+        void setSystemProperty(String key, String value) {
+            SystemProperties.set(key, value);
+        }
     }
 
     BugreportManagerServiceImpl(Context context) {
@@ -737,7 +741,7 @@
     @GuardedBy("mLock")
     private IDumpstate startAndGetDumpstateBinderServiceLocked() {
         // Start bugreport service.
-        SystemProperties.set("ctl.start", BUGREPORT_SERVICE);
+        mInjector.setSystemProperty("ctl.start", BUGREPORT_SERVICE);
 
         IDumpstate ds = null;
         boolean timedOut = false;
@@ -769,7 +773,7 @@
         // This tells init to cancel bugreportd service. Note that this is achieved through
         // setting a system property which is not thread-safe. So the lock here offers
         // thread-safety only among callers of the API.
-        SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
+        mInjector.setSystemProperty("ctl.stop", BUGREPORT_SERVICE);
     }
 
     @RequiresPermission(android.Manifest.permission.DUMP)
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 23d48e8..9af2b3f 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -389,7 +389,8 @@
      */
     boolean canLauncherAccessProfile(ComponentName launcherComponent, int userId) {
         if (android.os.Flags.allowPrivateProfile()
-                && Flags.enablePermissionToAccessHiddenProfiles()) {
+                && Flags.enablePermissionToAccessHiddenProfiles()
+                && Flags.enablePrivateSpaceFeatures()) {
             if (mUmInternal.getUserProperties(userId).getProfileApiVisibility()
                     != UserProperties.PROFILE_API_VISIBILITY_HIDDEN) {
                 return true;
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 9afdde5..b5476fd 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -760,13 +760,18 @@
         if (pkgName == null) {
             if (!mCrossProfileIntentResolverEngine.shouldSkipCurrentProfile(this, intent,
                     resolvedType, userId)) {
-                /*
-                 Check for results in the current profile only if there is no
-                 {@link CrossProfileIntentFilter} for user with flag
-                 {@link PackageManager.SKIP_CURRENT_PROFILE} set.
-                 */
-                result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this,
-                        intent, resolvedType, flags, userId), userId));
+
+                final List<ResolveInfo> queryResult = mComponentResolver.queryActivities(this,
+                        intent, resolvedType, flags, userId);
+                // If the user doesn't exist, the queryResult is null
+                if (queryResult != null) {
+                    /*
+                     Check for results in the current profile only if there is no
+                     {@link CrossProfileIntentFilter} for user with flag
+                     {@link PackageManager.SKIP_CURRENT_PROFILE} set.
+                     */
+                    result.addAll(filterIfNotSystemUser(queryResult, userId));
+                }
             }
             addInstant = isInstantAppResolutionAllowed(intent, result, userId,
                     false /*skipPackageCheck*/, flags);
@@ -788,9 +793,13 @@
 
             if (setting != null && setting.getAndroidPackage() != null && (resolveForStart
                     || !shouldFilterApplication(setting, filterCallingUid, userId))) {
-                result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this,
+                final List<ResolveInfo> queryResult = mComponentResolver.queryActivities(this,
                         intent, resolvedType, flags, setting.getAndroidPackage().getActivities(),
-                        userId), userId));
+                        userId);
+                // If the user doesn't exist, the queryResult is null
+                if (queryResult != null) {
+                    result.addAll(filterIfNotSystemUser(queryResult, userId));
+                }
             }
             if (result == null || result.size() == 0) {
                 // the caller wants to resolve for a particular package; however, there
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 6b56b85..c7ebb3c 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -584,7 +584,8 @@
             return android.os.Flags.allowPrivateProfile()
                     && Flags.enableHidingProfiles()
                     && Flags.enableLauncherAppsHiddenProfileChecks()
-                    && Flags.enablePermissionToAccessHiddenProfiles();
+                    && Flags.enablePermissionToAccessHiddenProfiles()
+                    && Flags.enablePrivateSpaceFeatures();
         }
 
         @VisibleForTesting // We override it in unit tests
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index ef8453d..29de26e 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -31,6 +31,7 @@
 import static android.content.pm.PackageManager.DELETE_ALL_USERS;
 import static android.content.pm.PackageManager.DELETE_ARCHIVE;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
+import static android.content.pm.PackageManager.INSTALL_UNARCHIVE;
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
 import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE;
@@ -754,8 +755,9 @@
 
         int draftSessionId;
         try {
-            draftSessionId = Binder.withCleanCallingIdentity(() ->
-                    createDraftSession(packageName, installerPackage, statusReceiver, userId));
+            draftSessionId = Binder.withCleanCallingIdentity(
+                    () -> createDraftSession(packageName, installerPackage, callerPackageName,
+                            statusReceiver, userId));
         } catch (RuntimeException e) {
             if (e.getCause() instanceof IOException) {
                 throw ExceptionUtils.wrap((IOException) e.getCause());
@@ -795,11 +797,18 @@
     }
 
     private int createDraftSession(String packageName, String installerPackage,
+            String callerPackageName,
             IntentSender statusReceiver, int userId) throws IOException {
         PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
                 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
         sessionParams.setAppPackageName(packageName);
-        sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT;
+        sessionParams.setAppLabel(
+                mContext.getString(com.android.internal.R.string.unarchival_session_app_label));
+        sessionParams.setAppIcon(
+                getArchivedAppIcon(packageName, UserHandle.of(userId), callerPackageName));
+        // To make sure SessionInfo::isUnarchival returns true for draft sessions,
+        // INSTALL_UNARCHIVE is also set.
+        sessionParams.installFlags = (INSTALL_UNARCHIVE_DRAFT | INSTALL_UNARCHIVE);
 
         int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId);
         // Handles case of repeated unarchival calls for the same package.
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index fe65010..59d6219 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -3386,12 +3386,7 @@
 
                     } else if (tagName.equals("verifier")) {
                         final String deviceIdentity = parser.getAttributeValue(null, "device");
-                        try {
-                            mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);
-                        } catch (IllegalArgumentException e) {
-                            Slog.w(PackageManagerService.TAG, "Discard invalid verifier device id: "
-                                    + e.getMessage());
-                        }
+                        mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);
                     } else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {
                         // No longer used.
                     } else if (tagName.equals("keyset-settings")) {
@@ -3419,7 +3414,8 @@
                 }
 
                 str.close();
-            } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) {
+            } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException
+                     | IllegalArgumentException e) {
                 // Remove corrupted file and retry.
                 atomicFile.failRead(str, e);
 
@@ -4558,6 +4554,10 @@
             for (int i = 0; i < size; i++) {
                 final PackageSetting ps = mPackages.valueAt(i);
                 if (ps.getPkg() == null) {
+                    // This would force-create correct per-user state.
+                    ps.setInstalled(false, userHandle);
+                    // Make sure the app is excluded from storage mapping for this user.
+                    writeKernelMappingLPr(ps);
                     continue;
                 }
                 final boolean shouldMaybeInstall = ps.isSystem() &&
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index c1ab3f9..4c653f6 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -2833,7 +2833,8 @@
 
     @VisibleForTesting
     boolean areShortcutsSupportedOnHomeScreen(@UserIdInt int userId) {
-        if (!android.os.Flags.allowPrivateProfile() || !Flags.disablePrivateSpaceItemsOnHome()) {
+        if (!android.os.Flags.allowPrivateProfile() || !Flags.disablePrivateSpaceItemsOnHome()
+                || !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
             return true;
         }
         final long start = getStatStartTime();
@@ -3795,6 +3796,7 @@
                 }
 
                 final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+                final boolean archival = intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false);
 
                 switch (action) {
                     case Intent.ACTION_PACKAGE_ADDED:
@@ -3805,7 +3807,7 @@
                         }
                         break;
                     case Intent.ACTION_PACKAGE_REMOVED:
-                        if (!replacing) {
+                        if (!replacing || (replacing && archival)) {
                             handlePackageRemoved(packageName, userId);
                         }
                         break;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 7349755..88e7596 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -21,10 +21,15 @@
 import static android.content.Intent.EXTRA_USER_ID;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE;
+import static android.content.pm.PackageManager.FEATURE_EMBEDDED;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
 import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY;
 import static android.os.UserManager.DISALLOW_USER_SWITCH;
 import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY;
 import static android.os.UserManager.USER_OPERATION_ERROR_UNKNOWN;
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
 
 import static com.android.internal.app.SetScreenLockDialogActivity.EXTRA_ORIGIN_USER_ID;
 import static com.android.internal.app.SetScreenLockDialogActivity.LAUNCH_REASON_DISABLE_QUIET_MODE;
@@ -1006,9 +1011,17 @@
         emulateSystemUserModeIfNeeded();
     }
 
+    private boolean doesDeviceHardwareSupportPrivateSpace() {
+        return !mPm.hasSystemFeature(FEATURE_EMBEDDED, 0)
+                && !mPm.hasSystemFeature(FEATURE_WATCH, 0)
+                && !mPm.hasSystemFeature(FEATURE_LEANBACK, 0)
+                && !mPm.hasSystemFeature(FEATURE_AUTOMOTIVE, 0);
+    }
+
     private static boolean isAutoLockForPrivateSpaceEnabled() {
         return android.os.Flags.allowPrivateProfile()
-                && Flags.supportAutolockForPrivateSpace();
+                && Flags.supportAutolockForPrivateSpace()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures();
     }
 
     void systemReady() {
@@ -1052,7 +1065,8 @@
 
     private boolean isAutoLockingPrivateSpaceOnRestartsEnabled() {
         return android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.enablePrivateSpaceAutolockOnRestarts();
+                && android.multiuser.Flags.enablePrivateSpaceAutolockOnRestarts()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures();
     }
 
     /**
@@ -1493,7 +1507,8 @@
     private boolean isProfileHidden(int userId) {
         UserProperties userProperties = getUserPropertiesCopy(userId);
         if (android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.enableHidingProfiles()) {
+                && android.multiuser.Flags.enableHidingProfiles()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
             return userProperties.getProfileApiVisibility()
                     == UserProperties.PROFILE_API_VISIBILITY_HIDDEN;
         }
@@ -1693,7 +1708,8 @@
                 setQuietModeEnabled(userId, true /* enableQuietMode */, target, callingPackage);
                 return true;
             }
-            if (android.os.Flags.allowPrivateProfile()) {
+            if (android.os.Flags.allowPrivateProfile()
+                    && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
                 final UserProperties userProperties = getUserPropertiesInternal(userId);
                 if (userProperties != null
                         && userProperties.isAuthAlwaysRequiredToDisableQuietMode()) {
@@ -1839,7 +1855,8 @@
         logQuietModeEnabled(userId, enableQuietMode, callingPackage);
 
         // Broadcast generic intents for all profiles
-        if (android.os.Flags.allowPrivateProfile()) {
+        if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
             broadcastProfileAvailabilityChanges(profile, parent.getUserHandle(),
                     enableQuietMode, false);
         }
@@ -1852,7 +1869,8 @@
 
     private void stopUserForQuietMode(int userId) throws RemoteException {
         if (android.os.Flags.allowPrivateProfile()
-                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()) {
+                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
             // Allow delayed locking since some profile types want to be able to unlock again via
             // biometrics.
             ActivityManager.getService()
@@ -2751,6 +2769,18 @@
     }
 
     @Override
+    public boolean canAddPrivateProfile(@UserIdInt int userId) {
+        checkCreateUsersPermission("canHaveRestrictedProfile");
+        UserInfo parentUserInfo = getUserInfo(userId);
+        return isUserTypeEnabled(USER_TYPE_PROFILE_PRIVATE)
+                && canAddMoreProfilesToUser(USER_TYPE_PROFILE_PRIVATE,
+                    userId, /* allowedToRemoveOne */ false)
+                && (parentUserInfo != null && parentUserInfo.isMain())
+                && doesDeviceHardwareSupportPrivateSpace()
+                && !hasUserRestriction(UserManager.DISALLOW_ADD_PRIVATE_PROFILE, userId);
+    }
+
+    @Override
     public boolean hasRestrictedProfiles(@UserIdInt int userId) {
         checkManageUsersPermission("hasRestrictedProfiles");
         synchronized (mUsersLock) {
@@ -5308,7 +5338,7 @@
         if (!isUserTypeEnabled(userTypeDetails)) {
             throwCheckedUserOperationException(
                     "Cannot add a user of disabled type " + userType + ".",
-                    UserManager.USER_OPERATION_ERROR_MAX_USERS);
+                    UserManager.USER_OPERATION_ERROR_DISABLED_USER);
         }
 
         synchronized (mUsersLock) {
@@ -5341,6 +5371,7 @@
         final boolean isDemo = UserManager.isUserTypeDemo(userType);
         final boolean isManagedProfile = UserManager.isUserTypeManagedProfile(userType);
         final boolean isCommunalProfile = UserManager.isUserTypeCommunalProfile(userType);
+        final boolean isPrivateProfile = UserManager.isUserTypePrivateProfile(userType);
 
         final long ident = Binder.clearCallingIdentity();
         UserInfo userInfo;
@@ -5387,6 +5418,12 @@
                                     + " for user " + parentId,
                             UserManager.USER_OPERATION_ERROR_MAX_USERS);
                 }
+                if (android.multiuser.Flags.blockPrivateSpaceCreation()
+                        && isPrivateProfile && !canAddPrivateProfile(parentId)) {
+                    throwCheckedUserOperationException(
+                            "Cannot add profile of type " + userType + " for user " + parentId,
+                            UserManager.USER_OPERATION_ERROR_PRIVATE_PROFILE);
+                }
                 if (isRestricted && (parentId != UserHandle.USER_SYSTEM)
                         && !isCreationOverrideEnabled()) {
                     throwCheckedUserOperationException(
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 114daaa..7f9c1cf 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -292,6 +292,7 @@
                 .setName(USER_TYPE_PROFILE_PRIVATE)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(1)
+                .setEnabled(UserManager.isPrivateProfileEnabled() ? 1 : 0)
                 .setLabels(R.string.profile_label_private)
                 .setIconBadge(com.android.internal.R.drawable.ic_private_profile_icon_badge)
                 .setBadgePlain(com.android.internal.R.drawable.ic_private_profile_badge)
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 305b087..5c8215e 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -16,6 +16,10 @@
 
 package com.android.server.pm.verify.domain;
 
+import static android.content.IntentFilter.WILDCARD;
+
+import static com.android.server.pm.verify.domain.DomainVerificationUtils.isValidDomain;
+
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 
@@ -253,9 +257,18 @@
             Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap =
                     pkgState.getUriRelativeFilterGroupMap();
             for (String domain : bundle.keySet()) {
+                if (!isValidDomain(domain)) {
+                    continue;
+                }
                 ArrayList<UriRelativeFilterGroupParcel> parcels =
                         bundle.getParcelableArrayList(domain, UriRelativeFilterGroupParcel.class);
-                domainToGroupsMap.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+                List<UriRelativeFilterGroup> groups =
+                        UriRelativeFilterGroup.parcelsToGroups(parcels);
+                if (groups == null || groups.isEmpty()) {
+                    domainToGroupsMap.remove(domain);
+                } else {
+                    domainToGroupsMap.put(domain, groups);
+                }
             }
         }
     }
@@ -273,9 +286,11 @@
                 Map<String, List<UriRelativeFilterGroup>> map =
                         pkgState.getUriRelativeFilterGroupMap();
                 for (int i = 0; i < domains.size(); i++) {
-                    List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
-                    bundle.putParcelableList(domains.get(i),
-                            UriRelativeFilterGroup.groupsToParcels(groups));
+                    if (map.containsKey(domains.get(i))) {
+                        List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
+                        bundle.putParcelableList(domains.get(i),
+                                UriRelativeFilterGroup.groupsToParcels(groups));
+                    }
                 }
             }
         }
@@ -285,15 +300,29 @@
     @NonNull
     private List<UriRelativeFilterGroup> getUriRelativeFilterGroups(@NonNull String packageName,
             @NonNull String domain) {
-        List<UriRelativeFilterGroup> groups = Collections.emptyList();
+        List<UriRelativeFilterGroup> groups;
         synchronized (mLock) {
             DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
             if (pkgState != null) {
-                groups = pkgState.getUriRelativeFilterGroupMap().getOrDefault(domain,
-                        Collections.emptyList());
+                Map<String, List<UriRelativeFilterGroup>> groupMap =
+                        pkgState.getUriRelativeFilterGroupMap();
+                groups = groupMap.get(domain);
+                if (groups != null) {
+                    return groups;
+                }
+                int first = domain.indexOf(".");
+                int second = domain.indexOf('.', first + 1);
+                while (first > 0 && second > 0) {
+                    groups = groupMap.get(WILDCARD + domain.substring(first));
+                    if (groups != null) {
+                        return groups;
+                    }
+                    first = second;
+                    second = domain.indexOf('.', second + 1);
+                }
             }
         }
-        return groups;
+        return Collections.emptyList();
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java
index 3fd00c6..b8c4d22 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationUtils.java
@@ -35,6 +35,9 @@
 
 public final class DomainVerificationUtils {
 
+    public static final int MAX_DOMAIN_LENGTH = 254;
+    public static final int MAX_DOMAIN_LABEL_LENGTH = 63;
+
     private static final ThreadLocal<Matcher> sCachedMatcher = ThreadLocal.withInitial(
             () -> Patterns.DOMAIN_NAME.matcher(""));
 
@@ -108,4 +111,41 @@
         appInfo.targetSdkVersion = pkg.getTargetSdkVersion();
         return appInfo;
     }
+
+    static boolean isValidDomain(String domain) {
+        if (domain.length() > MAX_DOMAIN_LENGTH || domain.equals("*")) {
+            return false;
+        }
+        if (domain.charAt(0) == '*') {
+            if (domain.charAt(1) != '.') {
+                return false;
+            }
+            domain = domain.substring(2);
+        }
+        int labels = 1;
+        int labelStart = -1;
+        for (int i = 0; i < domain.length(); i++) {
+            char c = domain.charAt(i);
+            if (c == '.') {
+                int labelLength = i - labelStart - 1;
+                if (labelLength == 0 || labelLength > MAX_DOMAIN_LABEL_LENGTH) {
+                    return false;
+                }
+                labelStart = i;
+                labels += 1;
+            } else if (!isValidDomainChar(c)) {
+                return false;
+            }
+        }
+        int lastLabelLength = domain.length() - labelStart - 1;
+        if (lastLabelLength == 0 || lastLabelLength > 63) {
+            return false;
+        }
+        return labels > 1;
+    }
+
+    private static boolean isValidDomainChar(char c) {
+        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+                || (c >= '0' && c <= '9') || c == '-';
+    }
 }
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 24d7acd..5360788 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -700,6 +700,8 @@
                     return runOverrideStatus();
                 case "reset":
                     return runReset();
+                case "headroom":
+                    return runHeadroom();
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -862,6 +864,36 @@
             }
         }
 
+        private int runHeadroom() {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                final PrintWriter pw = getOutPrintWriter();
+                int forecastSecs;
+                try {
+                    forecastSecs = Integer.parseInt(getNextArgRequired());
+                } catch (RuntimeException ex) {
+                    pw.println("Error: " + ex);
+                    return -1;
+                }
+                if (!mHalReady.get()) {
+                    pw.println("Error: thermal HAL is not ready");
+                    return -1;
+                }
+
+                if (forecastSecs < MIN_FORECAST_SEC || forecastSecs > MAX_FORECAST_SEC) {
+                    pw.println(
+                            "Error: forecast second input should be in range [" + MIN_FORECAST_SEC
+                                    + "," + MAX_FORECAST_SEC + "]");
+                    return -1;
+                }
+                float headroom = mTemperatureWatcher.getForecast(forecastSecs);
+                pw.println("Headroom in " + forecastSecs + " seconds: " + headroom);
+                return 0;
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
         @Override
         public void onHelp() {
             final PrintWriter pw = getOutPrintWriter();
@@ -877,6 +909,9 @@
             pw.println("    status code is defined in android.os.Temperature.");
             pw.println("  reset");
             pw.println("    unlocks the thermal status of the device.");
+            pw.println("  headroom FORECAST_SECONDS");
+            pw.println("    gets the thermal headroom forecast in specified seconds, from ["
+                    + MIN_FORECAST_SEC + "," + MAX_FORECAST_SEC + "].");
             pw.println();
         }
     }
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index f8c678a..52ef87c 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -45,11 +45,9 @@
 import static android.hardware.SensorPrivacyManager.Sources.QS_TILE;
 import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
 import static android.hardware.SensorPrivacyManager.Sources.SHELL;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
 import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED;
 import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED;
+import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED_EXCEPT_ALLOWLISTED_APPS;
 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_HARDWARE;
 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE;
 import static android.os.UserHandle.USER_NULL;
@@ -57,11 +55,9 @@
 
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
+import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__MICROPHONE;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__SENSOR_UNKNOWN;
@@ -98,7 +94,6 @@
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.graphics.drawable.Icon;
-import android.hardware.CameraPrivacyAllowlistEntry;
 import android.hardware.ISensorPrivacyListener;
 import android.hardware.ISensorPrivacyManager;
 import android.hardware.SensorPrivacyManager;
@@ -153,7 +148,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 
@@ -170,18 +164,12 @@
 
     public static final int REMINDER_DIALOG_DELAY_MILLIS = 500;
     @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-    @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-    @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-    @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
     private static final int ACTION__TOGGLE_ON =
             PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
     @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
+    private static final int ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS =
+            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS;
+    @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
     private static final int ACTION__TOGGLE_OFF =
             PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
     @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
@@ -208,8 +196,7 @@
     private CallStateHelper mCallStateHelper;
     private KeyguardManager mKeyguardManager;
 
-    List<CameraPrivacyAllowlistEntry> mCameraPrivacyAllowlist =
-            new ArrayList<CameraPrivacyAllowlistEntry>();
+    List<String> mCameraPrivacyAllowlist = new ArrayList<String>();
 
     private int mCurrentUser = USER_NULL;
 
@@ -227,14 +214,8 @@
         mPackageManagerInternal = getLocalService(PackageManagerInternal.class);
         mNotificationManager = mContext.getSystemService(NotificationManager.class);
         mSensorPrivacyServiceImpl = new SensorPrivacyServiceImpl();
-        ArrayMap<String, Boolean> cameraPrivacyAllowlist =
-                SystemConfig.getInstance().getCameraPrivacyAllowlist();
-
-        for (Map.Entry<String, Boolean> entry : cameraPrivacyAllowlist.entrySet()) {
-            CameraPrivacyAllowlistEntry ent = new CameraPrivacyAllowlistEntry();
-            ent.packageName = entry.getKey();
-            ent.isMandatory =  entry.getValue();
-            mCameraPrivacyAllowlist.add(ent);
+        for (String entry : SystemConfig.getInstance().getCameraPrivacyAllowlist()) {
+            mCameraPrivacyAllowlist.add(entry);
         }
     }
 
@@ -908,14 +889,8 @@
                     case DISABLED :
                         logAction = ACTION__TOGGLE_ON;
                         break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
+                    case ENABLED_EXCEPT_ALLOWLISTED_APPS :
+                        logAction = ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS;
                         break;
                     default :
                         logAction = ACTION__ACTION_UNKNOWN;
@@ -981,11 +956,23 @@
         @Override
         @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
         @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist() {
+        public List<String> getCameraPrivacyAllowlist() {
             enforceObserveSensorPrivacyPermission();
             return mCameraPrivacyAllowlist;
         }
 
+        /**
+         * Sets camera privacy allowlist.
+         * @param allowlist List of automotive driver assistance packages for
+         * privacy allowlisting.
+         * @hide
+         */
+        @Override
+        public void setCameraPrivacyAllowlist(List<String> allowlist) {
+            enforceManageSensorPrivacyPermission();
+            mCameraPrivacyAllowlist =  new ArrayList<>(allowlist);
+        }
+
         @Override
         @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST)
         @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
@@ -1005,23 +992,9 @@
                 return true;
             } else if (state == DISABLED) {
                 return false;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if ((packageName.equals(entry.packageName)) && !entry.isMandatory) {
-                        return false;
-                    }
-                }
-                return true;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if ((packageName.equals(entry.packageName)) && entry.isMandatory) {
-                        return false;
-                    }
-                }
-                return true;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if (packageName.equals(entry.packageName)) {
+            } else if (state == ENABLED_EXCEPT_ALLOWLISTED_APPS) {
+                for (String entry : mCameraPrivacyAllowlist) {
+                    if (packageName.equals(entry)) {
                         return false;
                     }
                 }
@@ -1616,7 +1589,7 @@
                             setToggleSensorPrivacy(userId, SHELL, sensor, false);
                         }
                         break;
-                        case "automotive_driver_assistance_apps" : {
+                        case "enable_except_allowlisted_apps" : {
                             if (Flags.cameraPrivacyAllowlist()) {
                                 int sensor = sensorStrToId(getNextArgRequired());
                                 if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
@@ -1625,33 +1598,7 @@
                                 }
 
                                 setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_APPS);
-                            }
-                        }
-                        break;
-                        case "automotive_driver_assistance_helpful_apps" : {
-                            if (Flags.cameraPrivacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS);
-                            }
-                        }
-                        break;
-                        case "automotive_driver_assistance_required_apps" : {
-                            if (Flags.cameraPrivacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS);
+                                        ENABLED_EXCEPT_ALLOWLISTED_APPS);
                             }
                         }
                         break;
@@ -1679,18 +1626,9 @@
                     pw.println("");
                     if (Flags.cameraPrivacyAllowlist()) {
                         if (isAutomotive(mContext)) {
-                            pw.println("  automotive_driver_assistance_apps USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which help you"
-                                    + " drive and apps which are required by OEM");
-                            pw.println("");
-                            pw.println("  automotive_driver_assistance_helpful_apps "
+                            pw.println("  enable_except_allowlisted_apps "
                                     + "USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which "
-                                    + "help you drive.");
-                            pw.println("");
-                            pw.println("  automotive_driver_assistance_required_apps "
-                                    + "USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which are "
+                            pw.println("    Enable privacy except for automotive apps which are "
                                     + "required by OEM.");
                             pw.println("");
                         }
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index ffce50e..79adcb4 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -349,7 +349,6 @@
             }
         }
 
-        userState.mIAppMap.clear();
         userState.mAdServiceMap = adServiceMap;
     }
 
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 5b77433..2fc183d 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -532,7 +532,7 @@
             return false;
         }
 
-        if (Flags.keyboardCategoryEnabled()) {
+        if (Flags.keyboardCategoryEnabled() && mVibrationConfig.hasFixedKeyboardAmplitude()) {
             int category = callerInfo.attrs.getCategory();
             if (usage == USAGE_TOUCH && category == CATEGORY_KEYBOARD) {
                 // Keyboard touch has a different user setting.
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 601c7f4..5175b74 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -39,6 +39,7 @@
 import android.view.DisplayInfo;
 import android.view.View;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.utils.TimingsTraceAndSlog;
 
 import libcore.io.IoUtils;
@@ -65,7 +66,7 @@
      * Maximum acceptable parallax.
      * A value of 1 means "the additional width for parallax is at most 100% of the screen width"
      */
-    private static final float MAX_PARALLAX = 1f;
+    @VisibleForTesting static final float MAX_PARALLAX = 1f;
 
     /**
      * We define three ways to adjust a crop. These modes are used depending on the situation:
@@ -73,10 +74,9 @@
      *   - When going from folded to unfolded, we want to add content
      *   - For a screen rotation, we want to keep the same amount of content
      */
-    private static final int ADD = 1;
-    private static final int REMOVE = 2;
-    private static final int BALANCE = 3;
-
+    @VisibleForTesting static final int ADD = 1;
+    @VisibleForTesting static final int REMOVE = 2;
+    @VisibleForTesting static final int BALANCE = 3;
 
     private final WallpaperDisplayHelper mWallpaperDisplayHelper;
 
@@ -131,19 +131,23 @@
                     (bitmapSize.y - crop.height()) / 2);
             return crop;
         }
+
+        // If any suggested crop is invalid, fallback to case 1
+        for (int i = 0; i < suggestedCrops.size(); i++) {
+            Rect testCrop = suggestedCrops.valueAt(i);
+            if (testCrop == null || testCrop.left < 0 || testCrop.top < 0
+                    || testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) {
+                Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize);
+                return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+            }
+        }
+
         int orientation = getOrientation(displaySize);
 
         // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop
         Rect suggestedCrop = suggestedCrops.get(orientation);
         if (suggestedCrop != null) {
-            if (suggestedCrop.left < 0 || suggestedCrop.top < 0
-                    || suggestedCrop.right > bitmapSize.x || suggestedCrop.bottom > bitmapSize.y) {
-                Slog.w(TAG, "invalid suggested crop: " + suggestedCrop);
-                Rect fullImage = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
-                return getAdjustedCrop(fullImage, bitmapSize, displaySize, true, rtl, ADD);
-            } else {
                 return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD);
-            }
         }
 
         // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and
@@ -209,7 +213,8 @@
      * Given a crop, a displaySize for the orientation of that crop, compute the visible part of the
      * crop. This removes any additional width used for parallax. No-op if displaySize == null.
      */
-    private static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) {
+    @VisibleForTesting
+    static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) {
         if (displaySize == null) return crop;
         Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD);
         // only keep the visible part (without parallax)
@@ -240,12 +245,14 @@
      *     </li>
      * </ul>
      */
-    private static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize,
+    @VisibleForTesting
+    static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize,
             boolean parallax, boolean rtl, int mode) {
         Rect adjustedCrop = new Rect(crop);
         float cropRatio = ((float) crop.width()) / crop.height();
         float screenRatio = ((float) screenSize.x) / screenSize.y;
-        if (cropRatio >= screenRatio) {
+        if (cropRatio == screenRatio) return crop;
+        if (cropRatio > screenRatio) {
             if (!parallax) {
                 // rotate everything 90 degrees clockwise, compute the result, and rotate back
                 int newLeft = bitmapSize.y - crop.bottom;
@@ -274,6 +281,7 @@
                 }
             }
         } else {
+            // TODO (b/281648899) the third case is not always correct, fix that.
             int widthToAdd = mode == REMOVE ? 0
                     : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width())
                     : (int) (0.5 + crop.height() - crop.width());
@@ -644,6 +652,9 @@
         if (!success) {
             Slog.e(TAG, "Unable to apply new wallpaper");
             wallpaper.getCropFile().delete();
+            wallpaper.mCropHints.clear();
+            wallpaper.cropHint.set(0, 0, 0, 0);
+            wallpaper.mSampleSize = 1f;
         }
 
         if (wallpaper.getCropFile().exists()) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index 88e9672..0165d65 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -341,6 +341,7 @@
             } else {
                 wallpaper.cropHint.set(totalCropHint);
             }
+            wallpaper.mSampleSize = parser.getAttributeFloat(null, "sampleSize", 1f);
         } else {
             wallpaper.cropHint.set(totalCropHint);
         }
@@ -493,6 +494,7 @@
             out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top);
             out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right);
             out.attributeInt(null, "totalCropBottom", wallpaper.cropHint.bottom);
+            out.attributeFloat(null, "sampleSize", wallpaper.mSampleSize);
         } else if (!multiCrop()) {
             final DisplayData wpdData =
                     mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index e9c4096..043470f 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -30,6 +30,7 @@
 import android.os.Process;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.Slog;
 import android.webkit.IWebViewUpdateService;
@@ -37,6 +38,7 @@
 import android.webkit.WebViewProviderResponse;
 
 import com.android.internal.util.DumpUtils;
+import com.android.modules.expresslog.Histogram;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 
@@ -52,6 +54,14 @@
 
     private static final String TAG = "WebViewUpdateService";
 
+    private static final Histogram sPrepareWebViewInSystemServerLatency = new Histogram(
+            "webview.value_prepare_webview_in_system_server_latency",
+            new Histogram.ScaledRangeOptions(20, 0, 1, 1.5f));
+
+    private static final Histogram sAppWaitingForRelroCompletionDelay = new Histogram(
+            "webview.value_app_waiting_for_relro_completion_delay",
+            new Histogram.ScaledRangeOptions(20, 0, 1, 1.4f));
+
     private BroadcastReceiver mWebViewUpdatedReceiver;
     private WebViewUpdateServiceInterface mImpl;
 
@@ -132,7 +142,10 @@
     }
 
     public void prepareWebViewInSystemServer() {
+        long currentTimeMs = SystemClock.uptimeMillis();
         mImpl.prepareWebViewInSystemServer();
+        sPrepareWebViewInSystemServerLatency.logSample(
+                (float) (SystemClock.uptimeMillis() - currentTimeMs));
     }
 
     private static String packageNameFromIntent(Intent intent) {
@@ -204,8 +217,12 @@
                 throw new IllegalStateException("Cannot create a WebView from the SystemServer");
             }
 
+            long startTimeMs = SystemClock.uptimeMillis();
             final WebViewProviderResponse webViewProviderResponse =
                     WebViewUpdateService.this.mImpl.waitForAndGetProvider();
+            long endTimeMs = SystemClock.uptimeMillis();
+            sAppWaitingForRelroCompletionDelay.logSample((float) (endTimeMs - startTimeMs));
+
             if (webViewProviderResponse.packageInfo != null) {
                 grantVisibilityToCaller(
                         webViewProviderResponse.packageInfo.packageName, Binder.getCallingUid());
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
index 1d6ad6d..532ff98 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
@@ -23,12 +23,15 @@
 import android.os.AsyncTask;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.util.AndroidRuntimeException;
 import android.util.Slog;
 import android.webkit.UserPackage;
 import android.webkit.WebViewFactory;
 import android.webkit.WebViewProviderInfo;
 import android.webkit.WebViewProviderResponse;
 
+import com.android.modules.expresslog.Counter;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -357,6 +360,12 @@
                 mNumRelroCreationsFinished = 0;
                 mNumRelroCreationsStarted =
                     mSystemInterface.onWebViewProviderChanged(newPackage);
+                Counter.logIncrement("webview.value_on_webview_provider_changed_counter");
+                if (newPackage.packageName.equals(getDefaultWebViewPackage().packageName)) {
+                    Counter.logIncrement(
+                            "webview.value_on_webview_provider_changed_"
+                            + "with_default_package_counter");
+                }
                 // If the relro creations finish before we know the number of started creations
                 // we will have to do any cleanup/notifying here.
                 checkIfRelrosDoneLocked();
@@ -388,9 +397,15 @@
 
     @Override
     public WebViewProviderInfo getDefaultWebViewPackage() {
-        throw new IllegalStateException(
-                "getDefaultWebViewPackage shouldn't be called if update_service_v2 flag is"
-                        + " disabled.");
+        for (WebViewProviderInfo provider : getWebViewPackages()) {
+            if (provider.availableByDefault) {
+                return provider;
+            }
+        }
+
+        // This should be unreachable because the config parser enforces that there is at least
+        // one availableByDefault provider.
+        throw new AndroidRuntimeException("No available by default WebView Provider.");
     }
 
     private static class ProviderAndPackageInfo {
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 1f9d265..fb338ba 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -31,6 +31,8 @@
 import android.webkit.WebViewProviderInfo;
 import android.webkit.WebViewProviderResponse;
 
+import com.android.modules.expresslog.Counter;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -412,6 +414,12 @@
                 mNumRelroCreationsFinished = 0;
                 mNumRelroCreationsStarted =
                     mSystemInterface.onWebViewProviderChanged(newPackage);
+                Counter.logIncrement("webview.value_on_webview_provider_changed_counter");
+                if (newPackage.packageName.equals(getDefaultWebViewPackage().packageName)) {
+                    Counter.logIncrement(
+                            "webview.value_on_webview_provider_changed_"
+                            + "with_default_package_counter");
+                }
                 // If the relro creations finish before we know the number of started creations
                 // we will have to do any cleanup/notifying here.
                 checkIfRelrosDoneLocked();
@@ -479,6 +487,7 @@
      * for all users, otherwise use the default provider.
      */
     private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
+        Counter.logIncrement("webview.value_find_preferred_webview_package_counter");
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
         String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext);
@@ -508,12 +517,15 @@
             PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(mDefaultProvider);
             if (validityResult(mDefaultProvider, packageInfo) == VALIDITY_OK) {
                 return packageInfo;
+            } else {
+                Counter.logIncrement("webview.value_default_webview_package_invalid_counter");
             }
         } catch (NameNotFoundException e) {
             Slog.w(TAG, "Default WebView package (" + mDefaultProvider.packageName + ") not found");
         }
 
         // This should never happen during normal operation (only with modified system images).
+        Counter.logIncrement("webview.value_webview_not_usable_for_all_users_counter");
         mAnyWebViewInstalled = false;
         throw new WebViewPackageMissingException("Could not find a loadable WebView package");
     }
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 91eff18..4189988 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1209,6 +1209,7 @@
                 private boolean mShown;
                 private boolean mLastSurfaceShown;
                 private int mAlpha;
+                private int mPreviousAlpha;
 
                 private volatile boolean mInvalidated;
 
@@ -1344,6 +1345,7 @@
                     // using WindowManagerGlobalLock. Grab copies of these values before
                     // drawing on the canvas so that drawing can be performed outside of the lock.
                     int alpha;
+                    boolean redrawBounds;
                     Rect drawingRect = null;
                     Region drawingBounds = null;
                     synchronized (mService.mGlobalLock) {
@@ -1361,7 +1363,13 @@
                         mInvalidated = false;
 
                         alpha = mAlpha;
-                        if (alpha > 0) {
+                        // For b/325863281, we should ensure the drawn border path is cleared when
+                        // alpha = 0. Therefore, we cache the last used alpha when drawing as
+                        // mPreviousAlpha and check it here. If mPreviousAlpha > 0, which means
+                        // the border is showing now, then we should still redraw the clear path
+                        // on the canvas so the border is cleared.
+                        redrawBounds = mAlpha > 0 || mPreviousAlpha > 0;
+                        if (redrawBounds) {
                             drawingBounds = new Region(mBounds);
                             // Empty dirty rectangle means unspecified.
                             if (mDirtyRect.isEmpty()) {
@@ -1378,7 +1386,7 @@
 
                     final boolean showSurface;
                     // Draw without holding WindowManagerGlobalLock.
-                    if (alpha > 0) {
+                    if (redrawBounds) {
                         Canvas canvas = null;
                         try {
                             canvas = mSurface.lockCanvas(drawingRect);
@@ -1392,11 +1400,11 @@
                         mPaint.setAlpha(alpha);
                         canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint);
                         mSurface.unlockCanvasAndPost(canvas);
-                        showSurface = true;
-                    } else {
-                        showSurface = false;
+                        mPreviousAlpha = alpha;
                     }
 
+                    showSurface = alpha > 0;
+
                     if (showSurface && !mLastSurfaceShown) {
                         mTransaction.show(mSurfaceControl).apply();
                         mLastSurfaceShown = true;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 7d5aa96d..7ba953d 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -553,7 +553,6 @@
     boolean launchFailed;   // set if a launched failed, to abort on 2nd try
     boolean delayedResume;  // not yet resumed because of stopped app switches?
     boolean finishing;      // activity in pending finish list?
-    int configChangeFlags;  // which config values have changed
     private boolean keysPaused;     // has key dispatching been paused for it?
     int launchMode;         // the launch mode activity attribute.
     int lockTaskLaunchMode; // the lockTaskMode manifest attribute, subject to override
@@ -659,7 +658,7 @@
 
     boolean mVoiceInteraction;
 
-    private int mPendingRelaunchCount;
+    int mPendingRelaunchCount;
     long mRelaunchStartTime;
 
     // True if we are current in the process of removing this app token from the display
@@ -1295,10 +1294,6 @@
         if (mDeferHidingClient) {
             pw.println(prefix + "mDeferHidingClient=" + mDeferHidingClient);
         }
-        if (configChangeFlags != 0) {
-            pw.print(prefix); pw.print(" configChangeFlags=");
-                    pw.println(Integer.toHexString(configChangeFlags));
-        }
         if (mServiceConnectionsHolder != null) {
             pw.print(prefix); pw.print("connections="); pw.println(mServiceConnectionsHolder);
         }
@@ -3993,7 +3988,7 @@
         // If the display does not have running activity, the configuration may need to be
         // updated for restoring original orientation of the display.
         if (next == null) {
-            mRootWindowContainer.ensureVisibilityAndConfig(next, getDisplayId(),
+            mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */, mDisplayContent,
                     true /* deferResume */);
         }
         if (activityRemoved) {
@@ -4064,7 +4059,7 @@
             try {
                 if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this);
                 mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
-                        DestroyActivityItem.obtain(token, finishing, configChangeFlags));
+                        DestroyActivityItem.obtain(token, finishing));
             } catch (Exception e) {
                 // We can just ignore exceptions here...  if the process has crashed, our death
                 // notification will clean things up.
@@ -4106,8 +4101,6 @@
             }
         }
 
-        configChangeFlags = 0;
-
         return removedFromHistory;
     }
 
@@ -5026,7 +5019,7 @@
                 return PauseActivityItem.obtain(token);
             case STOPPING:
             case STOPPED:
-                return StopActivityItem.obtain(token, configChangeFlags);
+                return StopActivityItem.obtain(token);
             default:
                 // Do not send a result immediately if the activity is in state INITIALIZING,
                 // RESTARTING_PROCESS, FINISHING, DESTROYING, or DESTROYED.
@@ -6300,7 +6293,7 @@
             try {
                 mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                         PauseActivityItem.obtain(token, finishing, false /* userLeaving */,
-                                configChangeFlags, false /* dontReport */, mAutoEnteringPip));
+                                false /* dontReport */, mAutoEnteringPip));
             } catch (Exception e) {
                 Slog.w(TAG, "Exception thrown sending pause: " + intent.getComponent(), e);
             }
@@ -6470,12 +6463,6 @@
      * state to match that fact.
      */
     void completeResumeLocked() {
-        final boolean wasVisible = mVisibleRequested;
-        setVisibility(true);
-        if (!wasVisible) {
-            // Visibility has changed, so take a note of it so we call the TaskStackChangedListener
-            mTaskSupervisor.mAppVisibilitiesChangedSinceLastPause = true;
-        }
         idle = false;
         results = null;
         if (newIntents != null && newIntents.size() > 0) {
@@ -6614,7 +6601,7 @@
             EventLogTags.writeWmStopActivity(
                     mUserId, System.identityHashCode(this), shortComponentName);
             mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
-                    StopActivityItem.obtain(token, configChangeFlags));
+                    StopActivityItem.obtain(token));
 
             mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT);
         } catch (Exception e) {
@@ -9858,7 +9845,6 @@
 
         if (shouldRelaunchLocked(changes, mTmpConfig)) {
             // Aha, the activity isn't handling the change, so DIE DIE DIE.
-            configChangeFlags |= changes;
             if (mVisible && mAtmService.mTmpUpdateConfigurationResult.mIsUpdating
                     && !mTransitionController.isShellTransitionsEnabled()) {
                 startFreezingScreenLocked(app, mAtmService.mTmpUpdateConfigurationResult.changes);
@@ -9887,7 +9873,7 @@
                 ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
                         + "activity %s called by %s", this, Debug.getCallers(4));
             }
-            relaunchActivityLocked(preserveWindow);
+            relaunchActivityLocked(preserveWindow, changes);
 
             // All done...  tell the caller we weren't able to keep this activity around.
             return false;
@@ -10029,9 +10015,8 @@
                 | CONFIG_SCREEN_LAYOUT)) != 0;
     }
 
-    void relaunchActivityLocked(boolean preserveWindow) {
+    void relaunchActivityLocked(boolean preserveWindow, int configChangeFlags) {
         if (mAtmService.mSuppressResizeConfigChanges && preserveWindow) {
-            configChangeFlags = 0;
             return;
         }
         if (!preserveWindow) {
@@ -10104,8 +10089,6 @@
 
         // The activity may be waiting for stop, but that is no longer appropriate for it.
         mTaskSupervisor.mStoppingActivities.remove(this);
-
-        configChangeFlags = 0;
     }
 
     /**
@@ -10174,7 +10157,7 @@
         // {@link ActivityTaskManagerService.activityStopped}).
         try {
             mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
-                    StopActivityItem.obtain(token, 0 /* configChanges */));
+                    StopActivityItem.obtain(token));
         } catch (RemoteException e) {
             Slog.w(TAG, "Exception thrown during restart " + this, e);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 6ad056f..2c39c58 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1625,7 +1625,7 @@
         final ActivityRecord currentTop = startedActivityRootTask.topRunningActivity();
         if (currentTop != null && currentTop.shouldUpdateConfigForDisplayChanged()) {
             mRootWindowContainer.ensureVisibilityAndConfig(
-                    currentTop, currentTop.getDisplayId(), false /* deferResume */);
+                    currentTop, currentTop.mDisplayContent, false /* deferResume */);
         }
 
         if (!avoidMoveToFront() && mDoResume && mRootWindowContainer
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 2cda1f5..826e332 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -842,7 +842,7 @@
                 // Deferring resume here because we're going to launch new activity shortly.
                 // We don't want to perform a redundant launch of the same record while ensuring
                 // configurations and trying to resume top activity of focused root task.
-                mRootWindowContainer.ensureVisibilityAndConfig(r, r.getDisplayId(),
+                mRootWindowContainer.ensureVisibilityAndConfig(r, r.mDisplayContent,
                         true /* deferResume */);
             }
 
@@ -1011,7 +1011,8 @@
         if (andResume && readyToResume()) {
             // As part of the process of launching, ActivityThread also performs
             // a resume.
-            rootTask.minimalResumeActivityLocked(r);
+            r.setState(RESUMED, "realStartActivityLocked");
+            r.completeResumeLocked();
         } else {
             // This activity is not starting in the resumed state... which should look like we asked
             // it to pause+stop (but remain visible), and it has done so and reported back the
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index fa76774..83f44d2 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -311,7 +311,7 @@
             return mInsetsHint;
         }
         final WindowState win = mWindowContainer.asWindowState();
-        if (win != null && win.mGivenInsetsPending && win.mAttrs.providedInsets == null) {
+        if (win != null && win.mGivenInsetsPending) {
             return mInsetsHint;
         }
         if (mInsetsHintStale) {
@@ -520,37 +520,11 @@
         updateVisibility();
         mControl = new InsetsSourceControl(mSource.getId(), mSource.getType(), leash,
                 mClientVisible, surfacePosition, getInsetsHint());
-        mStateController.notifySurfaceTransactionReady(this, getSurfaceTransactionId(leash), true);
 
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource Control %s for target %s", mControl, mControlTarget);
     }
 
-    private long getSurfaceTransactionId(SurfaceControl leash) {
-        // Here returns mNativeObject (long) as the ID instead of the leash itself so that
-        // InsetsStateController won't keep referencing the leash unexpectedly.
-        return leash != null ? leash.mNativeObject : 0;
-    }
-
-    /**
-     * This is called when the surface transaction of the leash initialization has been committed.
-     *
-     * @param id Indicates which transaction is committed so that stale callbacks can be dropped.
-     */
-    void onSurfaceTransactionCommitted(long id) {
-        if (mIsLeashReadyForDispatching) {
-            return;
-        }
-        if (mControl == null) {
-            return;
-        }
-        if (id != getSurfaceTransactionId(mControl.getLeash())) {
-            return;
-        }
-        mIsLeashReadyForDispatching = true;
-        mStateController.notifySurfaceTransactionReady(this, 0, false);
-    }
-
     void startSeamlessRotation() {
         if (!mSeamlessRotating) {
             mSeamlessRotating = true;
@@ -571,6 +545,10 @@
         return true;
     }
 
+    void onSurfaceTransactionApplied() {
+        mIsLeashReadyForDispatching = true;
+    }
+
     void setClientVisible(boolean clientVisible) {
         if (mClientVisible == clientVisible) {
             return;
@@ -755,7 +733,6 @@
         public void onAnimationCancelled(SurfaceControl animationLeash) {
             if (mAdapter == this) {
                 mStateController.notifyControlRevoked(mControlTarget, InsetsSourceProvider.this);
-                mStateController.notifySurfaceTransactionReady(InsetsSourceProvider.this, 0, false);
                 mControl = null;
                 mControlTarget = null;
                 mAdapter = null;
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index ba578f6..6b9fcf4 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -34,7 +34,6 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
-import android.util.SparseLongArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
@@ -59,7 +58,6 @@
     private final DisplayContent mDisplayContent;
 
     private final SparseArray<InsetsSourceProvider> mProviders = new SparseArray<>();
-    private final SparseLongArray mSurfaceTransactionIds = new SparseLongArray();
     private final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>>
             mControlTargetProvidersMap = new ArrayMap<>();
     private final SparseArray<InsetsControlTarget> mIdControlTargetMap = new SparseArray<>();
@@ -362,32 +360,14 @@
         notifyPendingInsetsControlChanged();
     }
 
-    void notifySurfaceTransactionReady(InsetsSourceProvider provider, long id, boolean ready) {
-        if (ready) {
-            mSurfaceTransactionIds.put(provider.getSource().getId(), id);
-        } else {
-            mSurfaceTransactionIds.delete(provider.getSource().getId());
-        }
-    }
-
     private void notifyPendingInsetsControlChanged() {
         if (mPendingControlChanged.isEmpty()) {
             return;
         }
-        final int size = mSurfaceTransactionIds.size();
-        final SparseLongArray surfaceTransactionIds = new SparseLongArray(size);
-        for (int i = 0; i < size; i++) {
-            surfaceTransactionIds.append(
-                    mSurfaceTransactionIds.keyAt(i), mSurfaceTransactionIds.valueAt(i));
-        }
         mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
-            for (int i = 0; i < size; i++) {
-                final int sourceId = surfaceTransactionIds.keyAt(i);
-                final InsetsSourceProvider provider = mProviders.get(sourceId);
-                if (provider == null) {
-                    continue;
-                }
-                provider.onSurfaceTransactionCommitted(surfaceTransactionIds.valueAt(i));
+            for (int i = mProviders.size() - 1; i >= 0; i--) {
+                final InsetsSourceProvider provider = mProviders.valueAt(i);
+                provider.onSurfaceTransactionApplied();
             }
             final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
             int displayId = mDisplayContent.getDisplayId();
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 07a03eb..a75d3b6 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1742,30 +1742,21 @@
      *
      * @param starting                  The currently starting activity or {@code null} if there is
      *                                  none.
-     * @param displayId                 The id of the display where operation is executed.
+     * @param displayContent            The display where the operation is executed.
      * @param deferResume               Whether to defer resume while updating config.
-     * @return 'true' if starting activity was kept or wasn't provided, 'false' if it was relaunched
-     * because of configuration update.
      */
-    boolean ensureVisibilityAndConfig(ActivityRecord starting, int displayId, boolean deferResume) {
+    void ensureVisibilityAndConfig(@Nullable ActivityRecord starting,
+            @NonNull DisplayContent displayContent, boolean deferResume) {
         // First ensure visibility without updating the config just yet. We need this to know what
         // activities are affecting configuration now.
         // Passing null here for 'starting' param value, so that visibility of actual starting
         // activity will be properly updated.
         ensureActivitiesVisible(null /* starting */, false /* notifyClients */);
 
-        if (displayId == INVALID_DISPLAY) {
-            // The caller didn't provide a valid display id, skip updating config.
-            return true;
-        }
-
         // Force-update the orientation from the WindowManager, since we need the true configuration
         // to send to the client now.
-        final DisplayContent displayContent = getDisplayContent(displayId);
-        Configuration config = null;
-        if (displayContent != null) {
-            config = displayContent.updateOrientation(starting, true /* forceUpdate */);
-        }
+        final Configuration config =
+                displayContent.updateOrientation(starting, true /* forceUpdate */);
         // Visibilities may change so let the starting activity have a chance to report. Can't do it
         // when visibility is changed in each AppWindowToken because it may trigger wrong
         // configuration push because the visibility of some activities may not be updated yet.
@@ -1773,13 +1764,8 @@
             starting.reportDescendantOrientationChangeIfNeeded();
         }
 
-        if (displayContent != null) {
-            // Update the configuration of the activities on the display.
-            return displayContent.updateDisplayOverrideConfigurationLocked(config, starting,
-                    deferResume);
-        } else {
-            return true;
-        }
+        // Update the configuration of the activities on the display.
+        displayContent.updateDisplayOverrideConfigurationLocked(config, starting, deferResume);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 1353ff0..73aa307 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -978,7 +978,6 @@
         }
         effectiveUid = info.applicationInfo.uid;
         mIsEffectivelySystemApp = info.applicationInfo.isSystemApp();
-        stringName = null;
 
         if (info.targetActivity == null) {
             if (_intent != null) {
@@ -1045,6 +1044,7 @@
             updateTaskDescription();
         }
         mSupportsPictureInPicture = info.supportsPictureInPicture();
+        stringName = null;
 
         // Re-adding the task to Recents once updated
         if (inRecents) {
@@ -3751,9 +3751,11 @@
                 // Boost the adjacent TaskFragment for dimmer if needed.
                 final TaskFragment taskFragment = wc.asTaskFragment();
                 if (taskFragment != null && taskFragment.isEmbedded()) {
+                    taskFragment.mDimmerSurfaceBoosted = false;
                     final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
                     if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) {
                         adjacentTf.assignLayer(t, layer++);
+                        adjacentTf.mDimmerSurfaceBoosted = true;
                     }
                 }
 
@@ -4946,13 +4948,6 @@
         }
     }
 
-    void minimalResumeActivityLocked(ActivityRecord r) {
-        ProtoLog.v(WM_DEBUG_STATES, "Moving to RESUMED: %s (starting new instance) "
-                + "callers=%s", r, Debug.getCallers(5));
-        r.setState(RESUMED, "minimalResumeActivityLocked");
-        r.completeResumeLocked();
-    }
-
     void checkReadyForSleep() {
         if (shouldSleepActivities() && goToSleepIfPossible(false /* shuttingDown */)) {
             mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */);
@@ -5861,7 +5856,7 @@
             }
 
             mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */,
-                    mDisplayContent.mDisplayId, false /* deferResume */);
+                    mDisplayContent, false /* deferResume */);
         } finally {
             if (mTransitionController.isShellTransitionsEnabled()) {
                 mAtmService.continueWindowLayout();
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 85d81c4..597e901 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -216,6 +216,9 @@
     Dimmer mDimmer = Dimmer.DIMMER_REFACTOR
             ? new SmoothDimmer(this) : new LegacyDimmer(this);
 
+    /** {@code true} if the dimmer surface is boosted. {@code false} otherwise. */
+    boolean mDimmerSurfaceBoosted;
+
     /** Apply the dim layer on the embedded TaskFragment. */
     static final int EMBEDDED_DIM_AREA_TASK_FRAGMENT = 0;
 
@@ -1533,10 +1536,6 @@
 
             next.setState(RESUMED, "resumeTopActivity");
 
-            // Have the window manager re-evaluate the orientation of
-            // the screen based on the new activity order.
-            boolean notUpdated = true;
-
             // Activity should also be visible if set mLaunchTaskBehind to true (see
             // ActivityRecord#shouldBeVisibleIgnoringKeyguard()).
             if (shouldBeVisible(next)) {
@@ -1548,28 +1547,15 @@
                 // result of invisible window resize.
                 // TODO: Remove this once visibilities are set correctly immediately when
                 // starting an activity.
-                notUpdated = !mRootWindowContainer.ensureVisibilityAndConfig(next, getDisplayId(),
+                final int originalRelaunchingCount = next.mPendingRelaunchCount;
+                mRootWindowContainer.ensureVisibilityAndConfig(next, mDisplayContent,
                         false /* deferResume */);
-            }
-
-            if (notUpdated) {
-                // The configuration update wasn't able to keep the existing
-                // instance of the activity, and instead started a new one.
-                // We should be all done, but let's just make sure our activity
-                // is still at the top and schedule another run if something
-                // weird happened.
-                ActivityRecord nextNext = topRunningActivity();
-                ProtoLog.i(WM_DEBUG_STATES, "Activity config changed during resume: "
-                        + "%s, new next: %s", next, nextNext);
-                if (nextNext != next) {
-                    // Do over!
-                    mTaskSupervisor.scheduleResumeTopActivities();
+                if (next.mPendingRelaunchCount > originalRelaunchingCount) {
+                    // The activity is scheduled to relaunch, then ResumeActivityItem will be also
+                    // included (see ActivityRecord#relaunchActivityLocked) if it should resume.
+                    next.completeResumeLocked();
+                    return true;
                 }
-                if (!next.isVisibleRequested() || next.mAppStopped) {
-                    next.setVisibility(true);
-                }
-                next.completeResumeLocked();
-                return true;
             }
 
             try {
@@ -1652,17 +1638,7 @@
                 return true;
             }
 
-            // From this point on, if something goes wrong there is no way
-            // to recover the activity.
-            try {
-                next.completeResumeLocked();
-            } catch (Exception e) {
-                // If any exception gets thrown, toss away this
-                // activity and try the next one.
-                Slog.w(TAG, "Exception thrown during resume of " + next, e);
-                next.finishIfPossible("resume-exception", true /* oomAdj */);
-                return true;
-            }
+            next.completeResumeLocked();
         } else {
             // Whoops, need to restart this activity!
             if (!next.hasBeenLaunched) {
@@ -1881,7 +1857,7 @@
 
             mAtmService.getLifecycleManager().scheduleTransactionItem(prev.app.getThread(),
                     PauseActivityItem.obtain(prev.token, prev.finishing, userLeaving,
-                            prev.configChangeFlags, pauseImmediately, autoEnteringPip));
+                            pauseImmediately, autoEnteringPip));
         } catch (Exception e) {
             // Ignore exception, if process died other code will cleanup.
             Slog.w(TAG, "Exception thrown during pause", e);
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 7fc61e1..a84a99a 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -573,7 +573,7 @@
 
     // Capture the animation surface control for activity's main window
     static class StartingWindowAnimationAdaptor implements AnimationAdapter {
-        SurfaceControl mAnimationLeash;
+
         @Override
         public boolean getShowWallpaper() {
             return false;
@@ -582,14 +582,10 @@
         @Override
         public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
                 int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
-            mAnimationLeash = animationLeash;
         }
 
         @Override
         public void onAnimationCancelled(SurfaceControl animationLeash) {
-            if (mAnimationLeash == animationLeash) {
-                mAnimationLeash = null;
-            }
         }
 
         @Override
@@ -604,9 +600,6 @@
 
         @Override
         public void dump(PrintWriter pw, String prefix) {
-            pw.print(prefix + "StartingWindowAnimationAdaptor mCapturedLeash=");
-            pw.print(mAnimationLeash);
-            pw.println();
         }
 
         @Override
@@ -616,16 +609,16 @@
 
     static SurfaceControl applyStartingWindowAnimation(WindowState window) {
         final SurfaceControl.Transaction t = window.getPendingTransaction();
-        final Rect mainFrame = window.getRelativeFrame();
         final StartingWindowAnimationAdaptor adaptor = new StartingWindowAnimationAdaptor();
         window.startAnimation(t, adaptor, false, ANIMATION_TYPE_STARTING_REVEAL);
-        if (adaptor.mAnimationLeash == null) {
+        final SurfaceControl leash = window.getAnimationLeash();
+        if (leash == null) {
             Slog.e(TAG, "Cannot start starting window animation, the window " + window
                     + " was removed");
             return null;
         }
-        t.setPosition(adaptor.mAnimationLeash, mainFrame.left, mainFrame.top);
-        return adaptor.mAnimationLeash;
+        t.setPosition(leash, window.mSurfacePosition.x, window.mSurfacePosition.y);
+        return leash;
     }
 
     boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
@@ -696,7 +689,9 @@
                 removalInfo.roundedCornerRadius =
                         topActivity.mLetterboxUiController.getRoundedCornersRadius(mainWindow);
                 removalInfo.windowAnimationLeash = applyStartingWindowAnimation(mainWindow);
-                removalInfo.mainFrame = mainWindow.getRelativeFrame();
+                removalInfo.mainFrame = new Rect(mainWindow.getFrame());
+                removalInfo.mainFrame.offsetTo(mainWindow.mSurfacePosition.x,
+                        mainWindow.mSurfacePosition.y);
             }
         }
         try {
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 594043d..9b19a70 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -349,7 +349,7 @@
         final Rect lastWallpaperBounds = wallpaperWin.getParentFrame();
         int screenWidth = lastWallpaperBounds.width();
         int screenHeight = lastWallpaperBounds.height();
-        float screenRatio = ((float) screenWidth) / screenHeight;
+        float screenRatio = (float) screenWidth / screenHeight;
         Point screenSize = new Point(screenWidth, screenHeight);
 
         WallpaperWindowToken token = wallpaperWin.mToken.asWallpaperToken();
@@ -399,20 +399,32 @@
             Point bitmapSize = new Point(
                     wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight);
             SparseArray<Rect> cropHints = token.getCropHints();
-            wallpaperFrame = mWallpaperCropUtils.getCrop(
-                    screenSize, bitmapSize, cropHints, wallpaperWin.isRtl());
+            wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame()
+                    : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints,
+                            wallpaperWin.isRtl());
+            int frameWidth = wallpaperFrame.width();
+            int frameHeight = wallpaperFrame.height();
+            float frameRatio = (float) frameWidth / frameHeight;
 
-            cropZoom = wallpaperFrame.isEmpty() ? 1f
-                    : ((float) screenHeight) / wallpaperFrame.height() / wallpaperWin.mVScale;
+            // If the crop is proportionally wider/taller than the screen, scale it so that its
+            // height/width matches the screen height/width, and use the additional width/height
+            // for parallax (respectively).
+            boolean scaleHeight = frameRatio >= screenRatio;
+            cropZoom = wallpaperFrame.isEmpty() ? 1f : scaleHeight
+                    ? (float) screenHeight / frameHeight / wallpaperWin.mVScale
+                    : (float) screenWidth / frameWidth / wallpaperWin.mHScale;
 
-            // A positive x / y offset shifts the wallpaper to the right / bottom respectively.
-            cropOffsetX = -wallpaperFrame.left
-                    + (int) ((cropZoom - 1f) * wallpaperFrame.height() * screenRatio / 2f);
-            cropOffsetY = -wallpaperFrame.top
-                    + (int) ((cropZoom - 1f) * wallpaperFrame.height() / 2f);
+            // The dimensions of the frame, without the additional width or height for parallax.
+            float w = scaleHeight ? frameHeight * screenRatio : frameWidth;
+            float h = scaleHeight ? frameHeight : frameWidth / screenRatio;
 
-            diffWidth = (int) (wallpaperFrame.width() * wallpaperWin.mHScale) - screenWidth;
-            diffHeight = (int) (wallpaperFrame.height() * wallpaperWin.mVScale) - screenHeight;
+            // Note: a positive x/y offset shifts the wallpaper to the right/bottom respectively.
+            cropOffsetX = -wallpaperFrame.left + (int) ((cropZoom - 1f) * w / 2f);
+            cropOffsetY = -wallpaperFrame.top + (int) ((cropZoom - 1f) * h / 2f);
+
+            // Available width or height for parallax
+            diffWidth = (int) ((frameWidth - w) * wallpaperWin.mHScale);
+            diffHeight = (int) ((frameHeight - h) * wallpaperWin.mVScale);
         } else {
             wallpaperFrame = wallpaperWin.getFrame();
             cropZoom = 1f;
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 5e7f1cb..b43a454 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -28,7 +28,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.content.Context;
-import android.os.HandlerExecutor;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.TimeUtils;
@@ -70,8 +69,6 @@
 
     private Choreographer mChoreographer;
 
-    private final HandlerExecutor mExecutor;
-
     /**
      * Indicates whether we have an animation frame callback scheduled, which will happen at
      * vsync-app and then schedule the animation tick at the right time (vsync-sf).
@@ -83,7 +80,8 @@
      * A list of runnable that need to be run after {@link WindowContainer#prepareSurfaces} is
      * executed and the corresponding transaction is closed and applied.
      */
-    private ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>();
+    private final ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>();
+    private boolean mInExecuteAfterPrepareSurfacesRunnables;
 
     private final SurfaceControl.Transaction mTransaction;
 
@@ -94,7 +92,6 @@
         mTransaction = service.mTransactionFactory.get();
         service.mAnimationHandler.runWithScissors(
                 () -> mChoreographer = Choreographer.getSfInstance(), 0 /* timeout */);
-        mExecutor = new HandlerExecutor(service.mAnimationHandler);
 
         mAnimationFrameCallback = frameTimeNs -> {
             synchronized (mService.mGlobalLock) {
@@ -200,19 +197,6 @@
             updateRunningExpensiveAnimationsLegacy();
         }
 
-        final ArrayList<Runnable> afterPrepareSurfacesRunnables = mAfterPrepareSurfacesRunnables;
-        if (!afterPrepareSurfacesRunnables.isEmpty()) {
-            mAfterPrepareSurfacesRunnables = new ArrayList<>();
-            mTransaction.addTransactionCommittedListener(mExecutor, () -> {
-                synchronized (mService.mGlobalLock) {
-                    // Traverse in order they were added.
-                    for (int i = 0, size = afterPrepareSurfacesRunnables.size(); i < size; i++) {
-                        afterPrepareSurfacesRunnables.get(i).run();
-                    }
-                    afterPrepareSurfacesRunnables.clear();
-                }
-            });
-        }
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyTransaction");
         mTransaction.apply();
         Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
@@ -220,6 +204,7 @@
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate");
 
         mService.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
+        executeAfterPrepareSurfacesRunnables();
 
         if (DEBUG_WINDOW_TRACE) {
             Slog.i(TAG, "!!! animate: exit"
@@ -301,10 +286,34 @@
 
     /**
      * Adds a runnable to be executed after {@link WindowContainer#prepareSurfaces} is called and
-     * the corresponding transaction is closed, applied, and committed.
+     * the corresponding transaction is closed and applied.
      */
     void addAfterPrepareSurfacesRunnable(Runnable r) {
+        // If runnables are already being handled in executeAfterPrepareSurfacesRunnable, then just
+        // immediately execute the runnable passed in.
+        if (mInExecuteAfterPrepareSurfacesRunnables) {
+            r.run();
+            return;
+        }
+
         mAfterPrepareSurfacesRunnables.add(r);
         scheduleAnimation();
     }
+
+    void executeAfterPrepareSurfacesRunnables() {
+
+        // Don't even think about to start recursing!
+        if (mInExecuteAfterPrepareSurfacesRunnables) {
+            return;
+        }
+        mInExecuteAfterPrepareSurfacesRunnables = true;
+
+        // Traverse in order they were added.
+        final int size = mAfterPrepareSurfacesRunnables.size();
+        for (int i = 0; i < size; i++) {
+            mAfterPrepareSurfacesRunnables.get(i).run();
+        }
+        mAfterPrepareSurfacesRunnables.clear();
+        mInExecuteAfterPrepareSurfacesRunnables = false;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ae5a5cb..a055db2 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2109,7 +2109,15 @@
                         + ", touchableRegion=" + w.mGivenTouchableRegion + " -> " + touchableRegion
                         + ", touchableInsets " + w.mTouchableInsets + " -> " + touchableInsets);
                 if (w != null) {
+                    final boolean wasGivenInsetsPending = w.mGivenInsetsPending;
                     w.mGivenInsetsPending = false;
+                    if ((!wasGivenInsetsPending || !w.hasInsetsSourceProvider())
+                            && w.mTouchableInsets == touchableInsets
+                            && w.mGivenContentInsets.equals(contentInsets)
+                            && w.mGivenVisibleInsets.equals(visibleInsets)
+                            && w.mGivenTouchableRegion.equals(touchableRegion)) {
+                        return;
+                    }
                     w.mGivenContentInsets.set(contentInsets);
                     w.mGivenVisibleInsets.set(visibleInsets);
                     w.mGivenTouchableRegion.set(touchableRegion);
@@ -9214,6 +9222,11 @@
             return false;
         }
 
+        if (taskFragment.mDimmerSurfaceBoosted) {
+            // Skip if the TaskFragment currently has dimmer surface boosted.
+            return false;
+        }
+
         final ActivityRecord topActivity =
                 taskFragment.getTask().topRunningActivity(true /* focusableOnly */);
         if (topActivity == null || topActivity == focusedWindow.mActivityRecord) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 0b29f96..a6db310f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -49,6 +49,7 @@
 
 import com.android.internal.os.ByteTransferPipe;
 import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.PerfettoProtoLogImpl;
 import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.IoThread;
@@ -111,8 +112,13 @@
                 case "logging":
                     IProtoLog instance = ProtoLog.getSingleInstance();
                     int result = 0;
-                    if (instance instanceof LegacyProtoLogImpl) {
-                        result = ((LegacyProtoLogImpl) instance).onShellCommand(this);
+                    if (instance instanceof LegacyProtoLogImpl
+                            || instance instanceof PerfettoProtoLogImpl) {
+                        if (instance instanceof LegacyProtoLogImpl) {
+                            result = ((LegacyProtoLogImpl) instance).onShellCommand(this);
+                        } else {
+                            result = ((PerfettoProtoLogImpl) instance).onShellCommand(this);
+                        }
                         if (result != 0) {
                             pw.println("Not handled, please use "
                                     + "`adb shell dumpsys activity service SystemUIService "
@@ -120,8 +126,7 @@
                         }
                     } else {
                         result = -1;
-                        pw.println("Command not supported. "
-                                + "Only supported when using legacy ProtoLog.");
+                        pw.println("ProtoLog impl doesn't support handling commands");
                     }
                     return result;
                 case "user-rotation":
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 18ac0e7..46bac16 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -695,7 +695,8 @@
      */
     private boolean mDrawnStateEvaluated;
 
-    private final Point mSurfacePosition = new Point();
+    /** The surface position relative to the parent container. */
+    final Point mSurfacePosition = new Point();
 
     /**
      * A region inside of this window to be excluded from touch.
@@ -1342,7 +1343,7 @@
             // This window doesn't provide any insets.
             return;
         }
-        if (mGivenInsetsPending && mAttrs.providedInsets == null) {
+        if (mGivenInsetsPending) {
             // The given insets are pending, and they are not reliable for now. The source frame
             // should be updated after the new given insets are sent to window manager.
             return;
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index 6d5fc80..b0e71bd 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -109,6 +109,9 @@
             return;
         }
         synchronized (mEnabledLock) {
+            if (!android.tracing.Flags.perfettoProtologTracing()) {
+                ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
+            }
             logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
             mBuffer.resetBuffer();
             mEnabled = mEnabledLockFree = true;
@@ -136,6 +139,9 @@
             writeTraceToFileLocked();
             logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
         }
+        if (!android.tracing.Flags.perfettoProtologTracing()) {
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true);
+        }
     }
 
     /**
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index ccd9bd0..b2bdaa3 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -24,16 +24,17 @@
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedPrimitiveArray.h>
 #include <powermanager/PowerHalController.h>
+#include <powermanager/PowerHintSessionWrapper.h>
 #include <utils/Log.h>
 
 #include <unordered_map>
 
 #include "jni.h"
 
-using aidl::android::hardware::power::IPowerHintSession;
 using aidl::android::hardware::power::SessionHint;
 using aidl::android::hardware::power::SessionMode;
 using aidl::android::hardware::power::WorkDuration;
+using android::power::PowerHintSessionWrapper;
 
 using android::base::StringPrintf;
 
@@ -49,7 +50,7 @@
 } gWorkDurationInfo;
 
 static power::PowerHalController gPowerHalController;
-static std::unordered_map<jlong, std::shared_ptr<IPowerHintSession>> gSessionMap;
+static std::unordered_map<jlong, std::shared_ptr<PowerHintSessionWrapper>> gSessionMap;
 static std::mutex gSessionMapLock;
 
 static int64_t getHintSessionPreferredRate() {
@@ -76,45 +77,45 @@
 }
 
 static void pauseHintSession(JNIEnv* env, int64_t session_ptr) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->pause();
 }
 
 static void resumeHintSession(JNIEnv* env, int64_t session_ptr) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->resume();
 }
 
 static void closeHintSession(JNIEnv* env, int64_t session_ptr) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->close();
     std::unique_lock<std::mutex> sessionLock(gSessionMapLock);
     gSessionMap.erase(session_ptr);
 }
 
 static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDurationNanos) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->updateTargetWorkDuration(targetDurationNanos);
 }
 
 static void reportActualWorkDuration(int64_t session_ptr,
                                      const std::vector<WorkDuration>& actualDurations) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->reportActualWorkDuration(actualDurations);
 }
 
 static void sendHint(int64_t session_ptr, SessionHint hint) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->sendHint(hint);
 }
 
 static void setThreads(int64_t session_ptr, const std::vector<int32_t>& threadIds) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->setThreads(threadIds);
 }
 
 static void setMode(int64_t session_ptr, SessionMode mode, bool enabled) {
-    auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->setMode(mode, enabled);
 }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index 12f4407..6aeb4fd5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -225,7 +225,7 @@
 
         synchronized (mLock) {
             PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId);
-            if (Flags.devicePolicySizeTrackingEnabled() && false) {
+            if (Flags.devicePolicySizeTrackingInternalEnabled()) {
                 if (!handleAdminPolicySizeLimit(localPolicyState, enforcingAdmin, value,
                         policyDefinition, userId)) {
                     return;
@@ -350,7 +350,7 @@
             }
             PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId);
 
-            if (Flags.devicePolicySizeTrackingEnabled() && false) {
+            if (Flags.devicePolicySizeTrackingInternalEnabled()) {
                 decreasePolicySizeForAdmin(localPolicyState, enforcingAdmin);
             }
 
@@ -496,7 +496,7 @@
 
         synchronized (mLock) {
             PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition);
-            if (Flags.devicePolicySizeTrackingEnabled() && false) {
+            if (Flags.devicePolicySizeTrackingInternalEnabled()) {
                 if (!handleAdminPolicySizeLimit(globalPolicyState, enforcingAdmin, value,
                         policyDefinition, UserHandle.USER_ALL)) {
                     return;
@@ -568,7 +568,7 @@
         synchronized (mLock) {
             PolicyState<V> policyState = getGlobalPolicyStateLocked(policyDefinition);
 
-            if (Flags.devicePolicySizeTrackingEnabled() && false) {
+            if (Flags.devicePolicySizeTrackingInternalEnabled()) {
                 decreasePolicySizeForAdmin(policyState, enforcingAdmin);
             }
 
@@ -1892,7 +1892,7 @@
 
         private void writeEnforcingAdminSizeInner(TypedXmlSerializer serializer)
                 throws IOException {
-            if (Flags.devicePolicySizeTrackingEnabled() && false) {
+            if (Flags.devicePolicySizeTrackingInternalEnabled()) {
                 if (mAdminPolicySize != null) {
                     for (int i = 0; i < mAdminPolicySize.size(); i++) {
                         int userId = mAdminPolicySize.keyAt(i);
@@ -1916,7 +1916,7 @@
 
         private void writeMaxPolicySizeInner(TypedXmlSerializer serializer)
                 throws IOException {
-            if (!Flags.devicePolicySizeTrackingEnabled() || true) {
+            if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
                 return;
             }
             serializer.startTag(/* namespace= */ null, TAG_MAX_POLICY_SIZE_LIMIT);
@@ -2081,7 +2081,7 @@
 
         private void readMaxPolicySizeInner(TypedXmlPullParser parser)
                 throws XmlPullParserException, IOException {
-            if (!Flags.devicePolicySizeTrackingEnabled() || true) {
+            if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
                 return;
             }
             mPolicySizeLimit = parser.getAttributeInt(/* namespace= */ null, ATTR_POLICY_SUM_SIZE);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0f97f4a..c37946b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -27,6 +27,7 @@
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_AIRPLANE_MODE;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_APPS_CONTROL;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_RESTRICTIONS;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIO_OUTPUT;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_AUTOFILL;
@@ -83,7 +84,6 @@
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_TIME;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER;
-import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_VPN;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_WALLPAPER;
 import static android.Manifest.permission.MANAGE_DEVICE_POLICY_WIFI;
@@ -196,11 +196,11 @@
 import static android.app.admin.DevicePolicyManager.STATUS_DEVICE_ADMIN_NOT_SUPPORTED;
 import static android.app.admin.DevicePolicyManager.STATUS_HAS_DEVICE_OWNER;
 import static android.app.admin.DevicePolicyManager.STATUS_HAS_PAIRED;
+import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_ONLY_SYSTEM_USER;
 import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED;
 import static android.app.admin.DevicePolicyManager.STATUS_MANAGED_USERS_NOT_SUPPORTED;
 import static android.app.admin.DevicePolicyManager.STATUS_NONSYSTEM_USER_EXISTS;
 import static android.app.admin.DevicePolicyManager.STATUS_NOT_SYSTEM_USER;
-import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_ONLY_SYSTEM_USER;
 import static android.app.admin.DevicePolicyManager.STATUS_OK;
 import static android.app.admin.DevicePolicyManager.STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
 import static android.app.admin.DevicePolicyManager.STATUS_SYSTEM_USER;
@@ -12062,7 +12062,7 @@
         }
 
         if (packageList != null) {
-            if (!Flags.devicePolicySizeTrackingEnabled()) {
+            if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
                 for (String pkg : packageList) {
                     PolicySizeVerifier.enforceMaxPackageNameLength(pkg);
                 }
@@ -13771,7 +13771,7 @@
             return;
         }
 
-        if (!Flags.devicePolicySizeTrackingEnabled()) {
+        if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
             PolicySizeVerifier.enforceMaxStringLength(accountType, "account type");
         }
 
@@ -14385,7 +14385,7 @@
     public void setLockTaskPackages(ComponentName who, String callerPackageName, String[] packages)
             throws SecurityException {
         Objects.requireNonNull(packages, "packages is null");
-        if (!Flags.devicePolicySizeTrackingEnabled()) {
+        if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
             for (String pkg : packages) {
                 PolicySizeVerifier.enforceMaxPackageNameLength(pkg);
             }
@@ -23389,7 +23389,7 @@
                 DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG);
     }
 
-    private boolean isUnicornFlagEnabled() {
+    static boolean isUnicornFlagEnabled() {
         return false;
     }
 
@@ -24235,7 +24235,7 @@
 
     @Override
     public void setMaxPolicyStorageLimit(String callerPackageName, int storageLimit) {
-        if (!Flags.devicePolicySizeTrackingEnabled() || true) {
+        if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
             return;
         }
         CallerIdentity caller = getCallerIdentity(callerPackageName);
@@ -24247,7 +24247,7 @@
 
     @Override
     public int getMaxPolicyStorageLimit(String callerPackageName) {
-        if (!Flags.devicePolicySizeTrackingEnabled() || true) {
+        if (!Flags.devicePolicySizeTrackingInternalEnabled()) {
             return -1;
         }
         CallerIdentity caller = getCallerIdentity(callerPackageName);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index c108deaf..a7adc5b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -66,6 +66,10 @@
     private static final String LOG_TAG = "PolicyEnforcerCallbacks";
 
     static boolean setAutoTimezoneEnabled(@Nullable Boolean enabled, @NonNull Context context) {
+        if (!DevicePolicyManagerService.isUnicornFlagEnabled()) {
+            Slogf.w(LOG_TAG, "Trying to enforce setAutoTimezoneEnabled while flag is off.");
+            return true;
+        }
         return Binder.withCleanCallingIdentity(() -> {
             Objects.requireNonNull(context);
 
@@ -79,6 +83,10 @@
     static boolean setPermissionGrantState(
             @Nullable Integer grantState, @NonNull Context context, int userId,
             @NonNull PolicyKey policyKey) {
+        if (!DevicePolicyManagerService.isUnicornFlagEnabled()) {
+            Slogf.w(LOG_TAG, "Trying to enforce setPermissionGrantState while flag is off.");
+            return true;
+        }
         return Boolean.TRUE.equals(Binder.withCleanCallingIdentity(() -> {
             if (!(policyKey instanceof PackagePermissionPolicyKey)) {
                 throw new IllegalArgumentException("policyKey is not of type "
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
index 82d5247..209107e 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
@@ -37,6 +37,7 @@
 import android.util.ArraySet;
 import android.util.Dumpable;
 import android.view.Display;
+import android.view.DisplayInfo;
 import android.view.Surface;
 
 import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
@@ -65,6 +66,7 @@
     private final Handler mHandler = new Handler();
     private final PostureEstimator mPostureEstimator;
     private final DisplayManager mDisplayManager;
+    private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
 
     /**
      * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair
@@ -140,10 +142,11 @@
     public void onDisplayChanged(int displayId) {
         if (displayId == DEFAULT_DISPLAY) {
             final Display display = mDisplayManager.getDisplay(displayId);
+            display.getDisplayInfo(mDefaultDisplayInfo);
             int displayState = display.getState();
             boolean isDisplayOn = displayState == Display.STATE_ON;
             mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn);
-            mPostureEstimator.onDisplayRotationChanged(display.getRotation());
+            mPostureEstimator.onDisplayRotationChanged(mDefaultDisplayInfo.rotation);
         }
     }
 
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
index 8d01b7a..901f24d 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -48,6 +48,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
 import android.view.Display;
+import android.view.DisplayInfo;
 import android.view.Surface;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -629,7 +630,11 @@
     }
 
     private void sendScreenRotation(int rotation) {
-        when(mDisplay.getRotation()).thenReturn(rotation);
+        doAnswer(invocation -> {
+            final DisplayInfo displayInfo = invocation.getArgument(0);
+            displayInfo.rotation = rotation;
+            return null;
+        }).when(mDisplay).getDisplayInfo(any());
         mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
     }
 
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index fc2eb26..c0d988d 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -20,27 +20,63 @@
 import android.companion.virtual.VirtualDeviceManager
 import android.os.Handler
 import android.os.UserHandle
+import android.permission.flags.Flags
 import android.util.ArrayMap
 import android.util.ArraySet
+import android.util.LongSparseArray
+import android.util.Slog
+import android.util.SparseArray
 import android.util.SparseBooleanArray
 import android.util.SparseIntArray
 import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.util.IntPair
 import com.android.server.appop.AppOpsCheckingServiceInterface
 import com.android.server.appop.AppOpsCheckingServiceInterface.AppOpsModeChangedListener
 import com.android.server.permission.access.AccessCheckingService
 import com.android.server.permission.access.AppOpUri
+import com.android.server.permission.access.DevicePermissionUri
+import com.android.server.permission.access.GetStateScope
 import com.android.server.permission.access.PackageUri
+import com.android.server.permission.access.PermissionUri
 import com.android.server.permission.access.UidUri
+import com.android.server.permission.access.appop.AppOpModes.MODE_ALLOWED
+import com.android.server.permission.access.appop.AppOpModes.MODE_FOREGROUND
+import com.android.server.permission.access.appop.AppOpModes.MODE_IGNORED
 import com.android.server.permission.access.collection.forEachIndexed
 import com.android.server.permission.access.collection.set
+import com.android.server.permission.access.permission.AppIdPermissionPolicy
+import com.android.server.permission.access.permission.DevicePermissionPolicy
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.permission.access.permission.PermissionService
 
 class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingServiceInterface {
     private val packagePolicy =
         service.getSchemePolicy(PackageUri.SCHEME, AppOpUri.SCHEME) as PackageAppOpPolicy
     private val appIdPolicy =
         service.getSchemePolicy(UidUri.SCHEME, AppOpUri.SCHEME) as AppIdAppOpPolicy
+    private val permissionPolicy =
+        service.getSchemePolicy(UidUri.SCHEME, PermissionUri.SCHEME) as AppIdPermissionPolicy
+    private val devicePermissionPolicy =
+        service.getSchemePolicy(UidUri.SCHEME, DevicePermissionUri.SCHEME) as DevicePermissionPolicy
 
     private val context = service.context
+
+    // Maps appop code to its runtime permission
+    private val runtimeAppOpToPermissionNames = SparseArray<String>()
+
+    // Maps runtime permission to its appop codes
+    private val runtimePermissionNameToAppOp = ArrayMap<String, Int>()
+
+    private var foregroundableOps = SparseBooleanArray()
+
+    /* Maps foreground permissions to their background permission. Background permissions aren't
+    required to be runtime */
+    private val foregroundToBackgroundPermissionName = ArrayMap<String, String>()
+
+    /* Maps background permissions to their foreground permissions. Background permissions aren't
+    required to be runtime */
+    private val backgroundToForegroundPermissionNames = ArrayMap<String, ArraySet<String>>()
+
     private lateinit var handler: Handler
 
     @Volatile private var listeners = ArraySet<AppOpsModeChangedListener>()
@@ -69,11 +105,60 @@
     }
 
     override fun systemReady() {
-        // Not implemented because upgrades are handled automatically.
+        if (Flags.runtimePermissionAppopsMappingEnabled()) {
+            createPermissionAppOpMapping()
+            val permissionListener = OnPermissionFlagsChangedListener()
+            permissionPolicy.addOnPermissionFlagsChangedListener(permissionListener)
+            devicePermissionPolicy.addOnPermissionFlagsChangedListener(permissionListener)
+        }
+    }
+
+    private fun createPermissionAppOpMapping() {
+        val permissions = service.getState { with(permissionPolicy) { getPermissions() } }
+
+        for (appOpCode in 0 until AppOpsManager._NUM_OP) {
+            AppOpsManager.opToPermission(appOpCode)?.let { permissionName ->
+                // Multiple ops might map to a single permission but only one is considered the
+                // runtime appop calculations.
+                if (appOpCode == AppOpsManager.permissionToOpCode(permissionName)) {
+                    val permission = permissions[permissionName]!!
+                    if (permission.isRuntime) {
+                        runtimePermissionNameToAppOp[permissionName] = appOpCode
+                        runtimeAppOpToPermissionNames[appOpCode] = permissionName
+                        permission.permissionInfo.backgroundPermission?.let {
+                            backgroundPermissionName ->
+                            // Note: background permission may not be runtime,
+                            // e.g. microphone/camera.
+                            foregroundableOps[appOpCode] = true
+                            foregroundToBackgroundPermissionName[permissionName] =
+                                backgroundPermissionName
+                            backgroundToForegroundPermissionNames
+                                .getOrPut(backgroundPermissionName, ::ArraySet)
+                                .add(permissionName)
+                        }
+                    }
+                }
+            }
+        }
     }
 
     override fun getNonDefaultUidModes(uid: Int, persistentDeviceId: String): SparseIntArray {
-        return opNameMapToOpSparseArray(getUidModes(uid))
+        val appId = UserHandle.getAppId(uid)
+        val userId = UserHandle.getUserId(uid)
+        service.getState {
+            val modes =
+                with(appIdPolicy) { opNameMapToOpSparseArray(getAppOpModes(appId, userId)?.map) }
+            if (Flags.runtimePermissionAppopsMappingEnabled()) {
+                runtimePermissionNameToAppOp.forEachIndexed { _, permissionName, appOpCode ->
+                    val mode = getUidModeFromPermissionState(appId, userId, permissionName)
+                    if (mode != AppOpsManager.opToDefaultMode(appOpCode)) {
+                        modes[appOpCode] = mode
+                    }
+                }
+            }
+
+            return modes
+        }
     }
 
     override fun getNonDefaultPackageModes(packageName: String, userId: Int): SparseIntArray {
@@ -84,7 +169,13 @@
         val appId = UserHandle.getAppId(uid)
         val userId = UserHandle.getUserId(uid)
         val opName = AppOpsManager.opToPublicName(op)
-        return service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } }
+        val permissionName = runtimeAppOpToPermissionNames[op]
+
+        return if (!Flags.runtimePermissionAppopsMappingEnabled() || permissionName == null) {
+            service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } }
+        } else {
+            service.getState { getUidModeFromPermissionState(appId, userId, permissionName) }
+        }
     }
 
     private fun getUidModes(uid: Int): ArrayMap<String, Int>? {
@@ -93,13 +184,66 @@
         return service.getState { with(appIdPolicy) { getAppOpModes(appId, userId) } }?.map
     }
 
-    override fun setUidMode(uid: Int, persistentDeviceId: String, op: Int, mode: Int): Boolean {
+    private fun GetStateScope.getUidModeFromPermissionState(
+        appId: Int,
+        userId: Int,
+        permissionName: String
+    ): Int {
+        val permissionFlags =
+            with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) }
+        val backgroundPermissionName = foregroundToBackgroundPermissionName[permissionName]
+        val backgroundPermissionFlags =
+            if (backgroundPermissionName != null) {
+                with(permissionPolicy) {
+                    getPermissionFlags(appId, userId, backgroundPermissionName)
+                }
+            } else {
+                PermissionFlags.RUNTIME_GRANTED
+            }
+        val result = evaluateModeFromPermissionFlags(permissionFlags, backgroundPermissionFlags)
+        if (result != MODE_IGNORED) {
+            return result
+        }
+
+        val fullerPermissionName =
+            PermissionService.getFullerPermission(permissionName) ?: return result
+        return getUidModeFromPermissionState(appId, userId, fullerPermissionName)
+    }
+
+    private fun evaluateModeFromPermissionFlags(
+        foregroundFlags: Int,
+        backgroundFlags: Int = PermissionFlags.RUNTIME_GRANTED
+    ): Int =
+        if (PermissionFlags.isAppOpGranted(foregroundFlags)) {
+            if (PermissionFlags.isAppOpGranted(backgroundFlags)) {
+                MODE_ALLOWED
+            } else {
+                MODE_FOREGROUND
+            }
+        } else {
+            MODE_IGNORED
+        }
+
+    override fun setUidMode(uid: Int, persistentDeviceId: String, code: Int, mode: Int): Boolean {
+        if (
+            Flags.runtimePermissionAppopsMappingEnabled() && code in runtimeAppOpToPermissionNames
+        ) {
+            Slog.w(
+                LOG_TAG,
+                "Cannot set UID mode for runtime permission app op, uid = $uid," +
+                    " code = ${AppOpsManager.opToName(code)}," +
+                    " mode = ${AppOpsManager.modeToName(mode)}",
+                RuntimeException()
+            )
+            return false
+        }
+
         val appId = UserHandle.getAppId(uid)
         val userId = UserHandle.getUserId(uid)
-        val opName = AppOpsManager.opToPublicName(op)
-        var wasChanged = false
+        val appOpName = AppOpsManager.opToPublicName(code)
+        var wasChanged: Boolean
         service.mutateState {
-            wasChanged = with(appIdPolicy) { setAppOpMode(appId, userId, opName, mode) }
+            wasChanged = with(appIdPolicy) { setAppOpMode(appId, userId, appOpName, mode) }
         }
         return wasChanged
     }
@@ -114,10 +258,23 @@
     private fun getPackageModes(packageName: String, userId: Int): ArrayMap<String, Int>? =
         service.getState { with(packagePolicy) { getAppOpModes(packageName, userId) } }?.map
 
-    override fun setPackageMode(packageName: String, op: Int, mode: Int, userId: Int) {
-        val opName = AppOpsManager.opToPublicName(op)
+    override fun setPackageMode(packageName: String, appOpCode: Int, mode: Int, userId: Int) {
+        val appOpName = AppOpsManager.opToPublicName(appOpCode)
+
+        if (
+            Flags.runtimePermissionAppopsMappingEnabled() &&
+                appOpCode in runtimeAppOpToPermissionNames
+        ) {
+            Slog.w(
+                LOG_TAG,
+                "(packageName=$packageName, userId=$userId)'s appop state" +
+                    " for runtime op $appOpName should not be set directly.",
+                RuntimeException()
+            )
+            return
+        }
         service.mutateState {
-            with(packagePolicy) { setAppOpMode(packageName, userId, opName, mode) }
+            with(packagePolicy) { setAppOpMode(packageName, userId, appOpName, mode) }
         }
     }
 
@@ -128,7 +285,7 @@
     }
 
     override fun removePackage(packageName: String, userId: Int): Boolean {
-        var wasChanged = false
+        var wasChanged: Boolean
         service.mutateState {
             wasChanged = with(packagePolicy) { removeAppOpModes(packageName, userId) }
         }
@@ -158,6 +315,13 @@
                     this[AppOpsManager.strOpToOp(op)] = true
                 }
             }
+            if (Flags.runtimePermissionAppopsMappingEnabled()) {
+                foregroundableOps.forEachIndexed { _, op, _ ->
+                    if (getUidMode(uid, persistentDeviceId, op) == AppOpsManager.MODE_FOREGROUND) {
+                        this[op] = true
+                    }
+                }
+            }
         }
     }
 
@@ -168,6 +332,13 @@
                     this[AppOpsManager.strOpToOp(op)] = true
                 }
             }
+            if (Flags.runtimePermissionAppopsMappingEnabled()) {
+                foregroundableOps.forEachIndexed { _, op, _ ->
+                    if (getPackageMode(packageName, op, userId) == AppOpsManager.MODE_FOREGROUND) {
+                        this[op] = true
+                    }
+                }
+            }
         }
     }
 
@@ -189,9 +360,10 @@
         }
     }
 
-    inner class OnAppIdAppOpModeChangedListener : AppIdAppOpPolicy.OnAppOpModeChangedListener() {
+    private inner class OnAppIdAppOpModeChangedListener :
+        AppIdAppOpPolicy.OnAppOpModeChangedListener() {
         // (uid, appOpCode) -> newMode
-        val pendingChanges = ArrayMap<Pair<Int, Int>, Int>()
+        private val pendingChanges = LongSparseArray<Int>()
 
         override fun onAppOpModeChanged(
             appId: Int,
@@ -202,7 +374,7 @@
         ) {
             val uid = UserHandle.getUid(userId, appId)
             val appOpCode = AppOpsManager.strOpToOp(appOpName)
-            val key = Pair(uid, appOpCode)
+            val key = IntPair.of(uid, appOpCode)
 
             pendingChanges[key] = newMode
         }
@@ -211,13 +383,15 @@
             val listenersLocal = listeners
             pendingChanges.forEachIndexed { _, key, mode ->
                 listenersLocal.forEachIndexed { _, listener ->
-                    val uid = key.first
-                    val appOpCode = key.second
+                    val uid = IntPair.first(key)
+                    val appOpCode = IntPair.second(key)
 
-                    listener.onUidModeChanged(uid,
+                    listener.onUidModeChanged(
+                        uid,
                         appOpCode,
                         mode,
-                        VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)
+                        VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT
+                    )
                 }
             }
 
@@ -228,7 +402,7 @@
     private inner class OnPackageAppOpModeChangedListener :
         PackageAppOpPolicy.OnAppOpModeChangedListener() {
         // (packageName, userId, appOpCode) -> newMode
-        val pendingChanges = ArrayMap<Triple<String, Int, Int>, Int>()
+        private val pendingChanges = ArrayMap<Triple<String, Int, Int>, Int>()
 
         override fun onAppOpModeChanged(
             packageName: String,
@@ -258,4 +432,130 @@
             pendingChanges.clear()
         }
     }
+
+    private inner class OnPermissionFlagsChangedListener :
+        AppIdPermissionPolicy.OnPermissionFlagsChangedListener,
+        DevicePermissionPolicy.OnDevicePermissionFlagsChangedListener {
+        // (uid, deviceId, appOpCode) -> newMode
+        private val pendingChanges = ArrayMap<Triple<Int, String, Int>, Int>()
+
+        override fun onPermissionFlagsChanged(
+            appId: Int,
+            userId: Int,
+            permissionName: String,
+            oldFlags: Int,
+            newFlags: Int
+        ) {
+            onDevicePermissionFlagsChanged(
+                appId,
+                userId,
+                VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT,
+                permissionName,
+                oldFlags,
+                newFlags
+            )
+        }
+
+        override fun onDevicePermissionFlagsChanged(
+            appId: Int,
+            userId: Int,
+            deviceId: String,
+            permissionName: String,
+            oldFlags: Int,
+            newFlags: Int
+        ) {
+            backgroundToForegroundPermissionNames[permissionName]?.let { foregroundPermissions ->
+                // This is a background permission; there may be multiple foreground permissions
+                // affected.
+                foregroundPermissions.forEachIndexed { _, foregroundPermissionName ->
+                    runtimePermissionNameToAppOp[foregroundPermissionName]?.let { appOpCode ->
+                        val foregroundPermissionFlags =
+                            getPermissionFlags(appId, userId, foregroundPermissionName)
+                        addPendingChangedModeIfNeeded(
+                            appId,
+                            userId,
+                            deviceId,
+                            appOpCode,
+                            foregroundPermissionFlags,
+                            oldFlags,
+                            foregroundPermissionFlags,
+                            newFlags
+                        )
+                    }
+                }
+            }
+                ?: foregroundToBackgroundPermissionName[permissionName]?.let { backgroundPermission
+                    ->
+                    runtimePermissionNameToAppOp[permissionName]?.let { appOpCode ->
+                        val backgroundPermissionFlags =
+                            getPermissionFlags(appId, userId, backgroundPermission)
+                        addPendingChangedModeIfNeeded(
+                            appId,
+                            userId,
+                            deviceId,
+                            appOpCode,
+                            oldFlags,
+                            backgroundPermissionFlags,
+                            newFlags,
+                            backgroundPermissionFlags
+                        )
+                    }
+                }
+                ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode ->
+                    addPendingChangedModeIfNeeded(
+                        appId,
+                        userId,
+                        deviceId,
+                        appOpCode,
+                        oldFlags,
+                        PermissionFlags.RUNTIME_GRANTED,
+                        newFlags,
+                        PermissionFlags.RUNTIME_GRANTED
+                    )
+                }
+        }
+
+        private fun getPermissionFlags(appId: Int, userId: Int, permissionName: String): Int =
+            service.getState {
+                with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) }
+            }
+
+        private fun addPendingChangedModeIfNeeded(
+            appId: Int,
+            userId: Int,
+            deviceId: String,
+            appOpCode: Int,
+            oldForegroundFlags: Int,
+            oldBackgroundFlags: Int,
+            newForegroundFlags: Int,
+            newBackgroundFlags: Int,
+        ) {
+            val oldMode = evaluateModeFromPermissionFlags(oldForegroundFlags, oldBackgroundFlags)
+            val newMode = evaluateModeFromPermissionFlags(newForegroundFlags, newBackgroundFlags)
+
+            if (oldMode != newMode) {
+                val uid = UserHandle.getUid(userId, appId)
+                pendingChanges[Triple(uid, deviceId, appOpCode)] = newMode
+            }
+        }
+
+        override fun onStateMutated() {
+            val listenersLocal = listeners
+            pendingChanges.forEachIndexed { _, key, mode ->
+                listenersLocal.forEachIndexed { _, listener ->
+                    val uid = key.first
+                    val deviceId = key.second
+                    val appOpCode = key.third
+
+                    listener.onUidModeChanged(uid, appOpCode, mode, deviceId)
+                }
+            }
+
+            pendingChanges.clear()
+        }
+    }
+
+    companion object {
+        private val LOG_TAG = AppOpService::class.java.simpleName
+    }
 }
diff --git a/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt b/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt
new file mode 100644
index 0000000..827dd0e
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.access.collection
+
+import android.util.LongSparseArray
+
+inline fun <T> LongSparseArray<T>.allIndexed(predicate: (Int, Long, T) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
+            return false
+        }
+    }
+    return true
+}
+
+inline fun <T> LongSparseArray<T>.anyIndexed(predicate: (Int, Long, T) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            return true
+        }
+    }
+    return false
+}
+
+inline fun <T> LongSparseArray<T>.forEachIndexed(action: (Int, Long, T) -> Unit) {
+    for (index in 0 until size) {
+        action(index, keyAt(index), valueAt(index))
+    }
+}
+
+inline fun <T> LongSparseArray<T>.forEachReversedIndexed(action: (Int, Long, T) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, keyAt(index), valueAt(index))
+    }
+}
+
+inline fun <T> LongSparseArray<T>.getOrPut(key: Long, defaultValue: () -> T): T {
+    val index = indexOfKey(key)
+    return if (index >= 0) {
+        valueAt(index)
+    } else {
+        defaultValue().also { put(key, it) }
+    }
+}
+
+inline val <T> LongSparseArray<T>.lastIndex: Int
+    get() = size - 1
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> LongSparseArray<T>.minusAssign(key: Long) {
+    delete(key)
+}
+
+inline fun <T> LongSparseArray<T>.noneIndexed(predicate: (Int, Long, T) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            return false
+        }
+    }
+    return true
+}
+
+inline fun <T> LongSparseArray<T>.removeAllIndexed(predicate: (Int, Long, T) -> Boolean): Boolean {
+    var isChanged = false
+    forEachReversedIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            removeAt(index)
+            isChanged = true
+        }
+    }
+    return isChanged
+}
+
+inline fun <T> LongSparseArray<T>.retainAllIndexed(predicate: (Int, Long, T) -> Boolean): Boolean {
+    var isChanged = false
+    forEachReversedIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
+            removeAt(index)
+            isChanged = true
+        }
+    }
+    return isChanged
+}
+
+inline val <T> LongSparseArray<T>.size: Int
+    get() = size()
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) {
+    put(key, value)
+}
diff --git a/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt b/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt
new file mode 100644
index 0000000..a582431
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.server.permission.access.collection
+
+import android.util.SparseIntArray
+
+inline fun SparseIntArray.allIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
+            return false
+        }
+    }
+    return true
+}
+
+inline fun SparseIntArray.anyIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            return true
+        }
+    }
+    return false
+}
+
+inline fun SparseIntArray.forEachIndexed(action: (Int, Int, Int) -> Unit) {
+    for (index in 0 until size) {
+        action(index, keyAt(index), valueAt(index))
+    }
+}
+
+inline fun SparseIntArray.forEachReversedIndexed(action: (Int, Int, Int) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, keyAt(index), valueAt(index))
+    }
+}
+
+inline fun SparseIntArray.getOrPut(key: Int, defaultValue: () -> Int): Int {
+    val index = indexOfKey(key)
+    return if (index >= 0) {
+        valueAt(index)
+    } else {
+        defaultValue().also { put(key, it) }
+    }
+}
+
+inline val SparseIntArray.lastIndex: Int
+    get() = size - 1
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun SparseIntArray.minusAssign(key: Int) {
+    delete(key)
+}
+
+inline fun SparseIntArray.noneIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            return false
+        }
+    }
+    return true
+}
+
+fun SparseIntArray.remove(key: Int) {
+    delete(key)
+}
+
+fun SparseIntArray.remove(key: Int, defaultValue: Int): Int {
+    val index = indexOfKey(key)
+    return if (index >= 0) {
+        val oldValue = valueAt(index)
+        removeAt(index)
+        oldValue
+    } else {
+        defaultValue
+    }
+}
+
+inline fun SparseIntArray.removeAllIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean {
+    var isChanged = false
+    forEachReversedIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            removeAt(index)
+            isChanged = true
+        }
+    }
+    return isChanged
+}
+
+inline fun SparseIntArray.retainAllIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean {
+    var isChanged = false
+    forEachReversedIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
+            removeAt(index)
+            isChanged = true
+        }
+    }
+    return isChanged
+}
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun SparseIntArray.set(key: Int, value: Int) {
+    put(key, value)
+}
+
+inline val SparseIntArray.size: Int
+    get() = size()
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 1f65463..b32c544 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -1112,6 +1112,8 @@
         )
         enforceCallingOrSelfAnyPermission(
             "getAllPermissionStates",
+            Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+            Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
             Manifest.permission.GET_RUNTIME_PERMISSIONS
         )
 
@@ -2826,5 +2828,8 @@
             } else {
                 emptySet<String>()
             }
+
+        fun getFullerPermission(permissionName: String): String? =
+            FULLER_PERMISSIONS[permissionName]
     }
 }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index 66e0717..c54a94e 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -83,11 +83,7 @@
         }
 
         val bundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1, DOMAIN_2))
-        assertThat(bundle.keySet()).containsExactlyElementsIn(listOf(DOMAIN_1, DOMAIN_2))
-        assertThat(bundle.getParcelableArrayList(DOMAIN_1, UriRelativeFilterGroup::class.java))
-            .isEmpty()
-        assertThat(bundle.getParcelableArrayList(DOMAIN_2, UriRelativeFilterGroup::class.java))
-            .isEmpty()
+        assertThat(bundle.keySet()).isEmpty()
 
         val pathGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
         pathGroup.addUriRelativeFilter(
diff --git a/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
new file mode 100644
index 0000000..180f54e
--- /dev/null
+++ b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2019 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.net;
+
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
+import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.ipsec.ike.IkeKeyIdIdentification;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.net.VpnProfile;
+import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
+import com.android.net.module.util.ProxyUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.security.auth.x500.X500Principal;
+
+/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class Ikev2VpnProfileTest {
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final String USERNAME_STRING = "username";
+    private static final String PASSWORD_STRING = "pa55w0rd";
+    private static final String EXCL_LIST = "exclList";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
+    private static final int TEST_MTU = 1300;
+
+    private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
+            SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
+
+    private X509Certificate mUserCert;
+    private X509Certificate mServerRootCa;
+    private PrivateKey mPrivateKey;
+
+    @Before
+    public void setUp() throws Exception {
+        mServerRootCa = generateRandomCertAndKeyPair().cert;
+
+        final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
+        mUserCert = userCertKey.cert;
+        mPrivateKey = userCertKey.key;
+    }
+
+    private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);
+
+        builder.setBypassable(true);
+        builder.setProxy(mProxy);
+        builder.setMaxMtu(TEST_MTU);
+        builder.setMetered(true);
+
+        return builder;
+    }
+
+    @Test
+    public void testBuildValidProfileWithOptions() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        // Check non-auth parameters correctly stored
+        assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
+        assertEquals(IDENTITY_STRING, profile.getUserIdentity());
+        assertEquals(mProxy, profile.getProxyInfo());
+        assertTrue(profile.isBypassable());
+        assertTrue(profile.isMetered());
+        assertEquals(TEST_MTU, profile.getMaxMtu());
+        assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testBuildUsernamePasswordProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertEquals(USERNAME_STRING, profile.getUsername());
+        assertEquals(PASSWORD_STRING, profile.getPassword());
+        assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+        assertNull(profile.getPresharedKey());
+        assertNull(profile.getRsaPrivateKey());
+        assertNull(profile.getUserCert());
+    }
+
+    @Test
+    public void testBuildDigitalSignatureProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertEquals(profile.getUserCert(), mUserCert);
+        assertEquals(mPrivateKey, profile.getRsaPrivateKey());
+        assertEquals(profile.getServerRootCaCert(), mServerRootCa);
+
+        assertNull(profile.getPresharedKey());
+        assertNull(profile.getUsername());
+        assertNull(profile.getPassword());
+    }
+
+    @Test
+    public void testBuildPresharedKeyProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertArrayEquals(PSK_BYTES, profile.getPresharedKey());
+
+        assertNull(profile.getServerRootCaCert());
+        assertNull(profile.getUsername());
+        assertNull(profile.getPassword());
+        assertNull(profile.getRsaPrivateKey());
+        assertNull(profile.getUserCert());
+    }
+
+    @Test
+    public void testBuildWithAllowedAlgorithmsAead() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+
+        List<String> allowedAlgorithms =
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305);
+        builder.setAllowedAlgorithms(allowedAlgorithms);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testBuildWithAllowedAlgorithmsNormal() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+
+        List<String> allowedAlgorithms =
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.AUTH_AES_XCBC,
+                        IpSecAlgorithm.AUTH_AES_CMAC,
+                        IpSecAlgorithm.CRYPT_AES_CBC,
+                        IpSecAlgorithm.CRYPT_AES_CTR);
+        builder.setAllowedAlgorithms(allowedAlgorithms);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsEmptyList() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(new ArrayList<>());
+            fail("Expected exception due to no valid algorithm set");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsInvalidList() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
+            fail("Expected exception due to missing encryption");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC));
+            fail("Expected exception due to missing authentication");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
+            fail("Expected exception due to insecure algorithm");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1));
+            fail("Expected exception due to insecure algorithm");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildNoAuthMethodSet() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.build();
+            fail("Expected exception due to lack of auth method");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildExcludeLocalRoutesSet() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+        builder.setLocalRoutesExcluded(true);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+        assertTrue(profile.areLocalRoutesExcluded());
+
+        builder.setBypassable(false);
+        try {
+            builder.build();
+            fail("Expected exception because excludeLocalRoutes should be set only"
+                    + " on the bypassable VPN");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildInvalidMtu() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setMaxMtu(500);
+            fail("Expected exception due to too-small MTU");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private void verifyVpnProfileCommon(VpnProfile profile) {
+        assertEquals(SERVER_ADDR_STRING, profile.server);
+        assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
+        assertEquals(mProxy, profile.proxy);
+        assertTrue(profile.isBypassable);
+        assertTrue(profile.isMetered);
+        assertEquals(TEST_MTU, profile.maxMtu);
+    }
+
+    @Test
+    public void testPskConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        verifyVpnProfileCommon(profile);
+        assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);
+
+        // Check nothing else is set
+        assertEquals("", profile.username);
+        assertEquals("", profile.password);
+        assertEquals("", profile.ipsecUserCert);
+        assertEquals("", profile.ipsecCaCert);
+    }
+
+    @Test
+    public void testUsernamePasswordConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        verifyVpnProfileCommon(profile);
+        assertEquals(USERNAME_STRING, profile.username);
+        assertEquals(PASSWORD_STRING, profile.password);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
+
+        // Check nothing else is set
+        assertEquals("", profile.ipsecUserCert);
+        assertEquals("", profile.ipsecSecret);
+    }
+
+    @Test
+    public void testRsaConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
+                + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
+        verifyVpnProfileCommon(profile);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
+        assertEquals(
+                expectedSecret,
+                profile.ipsecSecret);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
+
+        // Check nothing else is set
+        assertEquals("", profile.username);
+        assertEquals("", profile.password);
+    }
+
+    @Test
+    public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.username = USERNAME_STRING;
+        profile.password = PASSWORD_STRING;
+        profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
+        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getUsername());
+        assertNull(result.getPassword());
+        assertNull(result.getUserCert());
+        assertNull(result.getRsaPrivateKey());
+        assertNull(result.getServerRootCaCert());
+    }
+
+    @Test
+    public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.ipsecSecret = new String(PSK_BYTES);
+        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getPresharedKey());
+        assertNull(result.getUserCert());
+        assertNull(result.getRsaPrivateKey());
+    }
+
+    @Test
+    public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.username = USERNAME_STRING;
+        profile.password = PASSWORD_STRING;
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getUsername());
+        assertNull(result.getPassword());
+        assertNull(result.getPresharedKey());
+    }
+
+    @Test
+    public void testPskConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testUsernamePasswordConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testRsaConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testBuildWithIkeTunConnParamsConvertToVpnProfile() throws Exception {
+        // Special keyId that contains delimiter character of VpnProfile
+        final byte[] keyId = "foo\0bar".getBytes();
+        final IkeTunnelConnectionParams tunnelParams = new IkeTunnelConnectionParams(
+                getTestIkeSessionParams(true /* testIpv6 */, new IkeKeyIdIdentification(keyId)),
+                CHILD_PARAMS);
+        final Ikev2VpnProfile ikev2VpnProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
+        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
+
+        assertEquals(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS, vpnProfile.type);
+
+        // Username, password, server, ipsecIdentifier, ipsecCaCert, ipsecSecret, ipsecUserCert and
+        // getAllowedAlgorithms should not be set if IkeTunnelConnectionParams is set.
+        assertEquals("", vpnProfile.server);
+        assertEquals("", vpnProfile.ipsecIdentifier);
+        assertEquals("", vpnProfile.username);
+        assertEquals("", vpnProfile.password);
+        assertEquals("", vpnProfile.ipsecCaCert);
+        assertEquals("", vpnProfile.ipsecSecret);
+        assertEquals("", vpnProfile.ipsecUserCert);
+        assertEquals(0, vpnProfile.getAllowedAlgorithms().size());
+
+        // IkeTunnelConnectionParams should stay the same.
+        assertEquals(tunnelParams, vpnProfile.ikeTunConnParams);
+
+        // Convert to disk-stable format and then back to Ikev2VpnProfile should be the same.
+        final VpnProfile decodedVpnProfile =
+                VpnProfile.decode(vpnProfile.key, vpnProfile.encode());
+        final Ikev2VpnProfile convertedIkev2VpnProfile =
+                Ikev2VpnProfile.fromVpnProfile(decodedVpnProfile);
+        assertEquals(ikev2VpnProfile, convertedIkev2VpnProfile);
+    }
+
+    @Test
+    public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
+        // Config authentication related fields is not required while building with
+        // IkeTunnelConnectionParams.
+        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAutomaticNattKeepaliveTimerEnabled(true);
+        builder.setAutomaticIpVersionSelectionEnabled(true);
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testAutomaticNattAndIpVersionDefaults() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled());
+        assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled());
+    }
+
+    @Test
+    public void testEquals() throws Exception {
+        // Verify building without IkeTunnelConnectionParams
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        assertEquals(builder.build(), builder.build());
+
+        // Verify building with IkeTunnelConnectionParams
+        final IkeTunnelConnectionParams tunnelParams =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
+        final IkeTunnelConnectionParams tunnelParams2 =
+                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
+        assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
+                new Ikev2VpnProfile.Builder(tunnelParams2).build());
+    }
+
+    @Test
+    public void testBuildProfileWithNullProxy() throws Exception {
+        final Ikev2VpnProfile ikev2VpnProfile =
+                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                        .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa)
+                        .build();
+
+        // ProxyInfo should be null for the profile without setting ProxyInfo.
+        assertNull(ikev2VpnProfile.getProxyInfo());
+
+        // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile()
+        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
+        assertNull(vpnProfile.proxy);
+
+        final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile);
+        assertNull(convertedIkev2VpnProfile.getProxyInfo());
+    }
+
+    private static class CertificateAndKey {
+        public final X509Certificate cert;
+        public final PrivateKey key;
+
+        CertificateAndKey(X509Certificate cert, PrivateKey key) {
+            this.cert = cert;
+            this.key = key;
+        }
+    }
+
+    private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
+        final Date validityBeginDate =
+                new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
+        final Date validityEndDate =
+                new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
+
+        // Generate a keypair
+        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(512);
+        final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+        final X500Principal dnName = new X500Principal("CN=test.android.com");
+        final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+        certGen.setSubjectDN(dnName);
+        certGen.setIssuerDN(dnName);
+        certGen.setNotBefore(validityBeginDate);
+        certGen.setNotAfter(validityEndDate);
+        certGen.setPublicKey(keyPair.getPublic());
+        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+        final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
+        return new CertificateAndKey(cert, keyPair.getPrivate());
+    }
+}
diff --git a/services/tests/VpnTests/java/android/net/VpnManagerTest.java b/services/tests/VpnTests/java/android/net/VpnManagerTest.java
new file mode 100644
index 0000000..f5b83f0
--- /dev/null
+++ b/services/tests/VpnTests/java/android/net/VpnManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.test.mock.MockContext;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.net.VpnProfile;
+import com.android.internal.util.MessageUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link VpnManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VpnManagerTest {
+
+    private static final String PKG_NAME = "fooPackage";
+
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
+
+    private IVpnManager mMockService;
+    private VpnManager mVpnManager;
+    private final MockContext mMockContext =
+            new MockContext() {
+                @Override
+                public String getOpPackageName() {
+                    return PKG_NAME;
+                }
+            };
+
+    @Before
+    public void setUp() throws Exception {
+        assumeFalse("Skipping test because watches don't support VPN",
+                InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_WATCH));
+        mMockService = mock(IVpnManager.class);
+        mVpnManager = new VpnManager(mMockContext, mMockService);
+    }
+
+    @Test
+    public void testProvisionVpnProfilePreconsented() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
+                .thenReturn(true);
+
+        // Expect there to be no intent returned, as consent has already been granted.
+        assertNull(mVpnManager.provisionVpnProfile(profile));
+        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testProvisionVpnProfileNeedsConsent() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
+                .thenReturn(false);
+
+        // Expect intent to be returned, as consent has not already been granted.
+        final Intent intent = mVpnManager.provisionVpnProfile(profile);
+        assertNotNull(intent);
+
+        final ComponentName expectedComponentName =
+                ComponentName.unflattenFromString(
+                        "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
+        assertEquals(expectedComponentName, intent.getComponent());
+        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testDeleteProvisionedVpnProfile() throws Exception {
+        mVpnManager.deleteProvisionedVpnProfile();
+        verify(mMockService).deleteVpnProfile(eq(PKG_NAME));
+    }
+
+    @Test
+    public void testStartProvisionedVpnProfile() throws Exception {
+        mVpnManager.startProvisionedVpnProfile();
+        verify(mMockService).startVpnProfile(eq(PKG_NAME));
+    }
+
+    @Test
+    public void testStopProvisionedVpnProfile() throws Exception {
+        mVpnManager.stopProvisionedVpnProfile();
+        verify(mMockService).stopVpnProfile(eq(PKG_NAME));
+    }
+
+    private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
+        return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                .setBypassable(true)
+                .setMaxMtu(1300)
+                .setMetered(true)
+                .setAuthPsk(PSK_BYTES)
+                .build();
+    }
+
+    @Test
+    public void testVpnTypesEqual() throws Exception {
+        SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames(
+                new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" });
+        SparseArray<String> nativeVpnType = MessageUtils.findMessageNames(
+                new Class[] { NativeVpnType.class }, new String[]{ "" });
+
+        // TYPE_VPN_NONE = -1 is only defined in VpnManager.
+        assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size());
+        for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) {
+            assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i));
+        }
+    }
+}
diff --git a/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
new file mode 100644
index 0000000..acbe8b8
--- /dev/null
+++ b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.net;
+
+import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpSecAlgorithm;
+import android.net.ipsec.ike.IkeTunnelConnectionParams;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Unit tests for {@link VpnProfile}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VpnProfileTest {
+    private static final String DUMMY_PROFILE_KEY = "Test";
+
+    private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
+    private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
+    private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25;
+    private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26;
+    private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27;
+    private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28;
+    private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29;
+
+    @Test
+    public void testDefaults() throws Exception {
+        final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
+
+        assertEquals(DUMMY_PROFILE_KEY, p.key);
+        assertEquals("", p.name);
+        assertEquals(VpnProfile.TYPE_PPTP, p.type);
+        assertEquals("", p.server);
+        assertEquals("", p.username);
+        assertEquals("", p.password);
+        assertEquals("", p.dnsServers);
+        assertEquals("", p.searchDomains);
+        assertEquals("", p.routes);
+        assertTrue(p.mppe);
+        assertEquals("", p.l2tpSecret);
+        assertEquals("", p.ipsecIdentifier);
+        assertEquals("", p.ipsecSecret);
+        assertEquals("", p.ipsecUserCert);
+        assertEquals("", p.ipsecCaCert);
+        assertEquals("", p.ipsecServerCert);
+        assertEquals(null, p.proxy);
+        assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
+        assertFalse(p.isBypassable);
+        assertFalse(p.isMetered);
+        assertEquals(1360, p.maxMtu);
+        assertFalse(p.areAuthParamsInline);
+        assertFalse(p.isRestrictedToTestNetworks);
+        assertFalse(p.excludeLocalRoutes);
+        assertFalse(p.requiresInternetValidation);
+        assertFalse(p.automaticNattKeepaliveTimerEnabled);
+        assertFalse(p.automaticIpVersionSelectionEnabled);
+    }
+
+    private VpnProfile getSampleIkev2Profile(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */,
+                true /* automaticIpVersionSelectionEnabled */);
+
+        p.name = "foo";
+        p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
+        p.server = "bar";
+        p.username = "baz";
+        p.password = "qux";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.l2tpSecret = "";
+        p.ipsecIdentifier = "quux";
+        p.ipsecSecret = "quuz";
+        p.ipsecUserCert = "corge";
+        p.ipsecCaCert = "grault";
+        p.ipsecServerCert = "garply";
+        p.proxy = null;
+        p.setAllowedAlgorithms(
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.CRYPT_AES_CBC));
+        p.isBypassable = true;
+        p.isMetered = true;
+        p.maxMtu = 1350;
+        p.areAuthParamsInline = true;
+
+        // Not saved, but also not compared.
+        p.saveLogin = true;
+
+        return p;
+    }
+
+    private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
+                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
+                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS),
+                true /* mAutomaticNattKeepaliveTimerEnabled */,
+                true /* automaticIpVersionSelectionEnabled */);
+
+        p.name = "foo";
+        p.server = "bar";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.proxy = null;
+        p.setAllowedAlgorithms(
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.CRYPT_AES_CBC));
+        p.isBypassable = true;
+        p.isMetered = true;
+        p.maxMtu = 1350;
+        p.areAuthParamsInline = true;
+
+        // Not saved, but also not compared.
+        p.saveLogin = true;
+
+        return p;
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(
+                getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
+
+        final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        modified.maxMtu--;
+        assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
+    }
+
+    @Test
+    public void testParcelUnparcel() {
+        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
+        assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
+    }
+
+    @Test
+    public void testEncodeDecodeWithIkeTunConnParams() {
+        final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testEncodeDecode() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testEncodeDecodeTooManyValues() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final byte[] tooManyValues =
+                (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
+
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
+    }
+
+    private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
+        // Sort to ensure when we remove, we can do it from greatest first.
+        Arrays.sort(missingIndices);
+
+        final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
+        final List<String> parts =
+                new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
+
+        // Remove from back first to ensure indexing is consistent.
+        for (int i = missingIndices.length - 1; i >= 0; i--) {
+            parts.remove(missingIndices[i]);
+        }
+
+        return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
+    }
+
+    @Test
+    public void testEncodeDecodeInvalidNumberOfValues() {
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_AUTH_PARAMS_INLINE,
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
+                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
+                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
+                        ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
+                        ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
+                        ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED
+                        /* missingIndices */);
+
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
+    }
+
+    private String getEncodedDecodedIkev2ProfileWithtooFewValues() {
+        return getEncodedDecodedIkev2ProfileMissingValues(
+                ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
+                ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
+                ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
+                ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
+                ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
+                ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without isRestrictedToTestNetworks defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.isRestrictedToTestNetworks);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingExcludeLocalRoutes() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without excludeLocalRoutes defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.excludeLocalRoutes);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingRequiresValidation() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without requiresValidation defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.requiresInternetValidation);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.automaticNattKeepaliveTimerEnabled);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without automaticIpVersionSelectionEnabled defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.automaticIpVersionSelectionEnabled);
+    }
+
+    @Test
+    public void testEncodeDecodeLoginsNotSaved() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        profile.saveLogin = false;
+
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertNotEquals(profile, decoded);
+
+        // Add the username/password back, everything else must be equal.
+        decoded.username = profile.username;
+        decoded.password = profile.password;
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testClone() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final VpnProfile clone = profile.clone();
+        assertEquals(profile, clone);
+        assertNotSame(profile, clone);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/LockdownVpnTrackerTest.java b/services/tests/VpnTests/java/com/android/server/net/LockdownVpnTrackerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/net/LockdownVpnTrackerTest.java
rename to services/tests/VpnTests/java/com/android/server/net/LockdownVpnTrackerTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index fbb14c3..4409051 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.when;
 
 import android.hardware.display.DisplayManagerInternal;
-import android.os.PowerManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -66,8 +65,6 @@
         mSession.stopOffload();
 
         assertFalse(mSession.isActive());
-        verify(mDisplayPowerController).setBrightnessFromOffload(
-                PowerManager.BRIGHTNESS_INVALID_FLOAT);
 
         // An inactive session shouldn't be stopped again
         mSession.stopOffload();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index e9315c8..14d8a9c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -22,6 +22,7 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -77,6 +78,7 @@
 import com.android.server.am.BatteryStatsService;
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.BrightnessReason;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
@@ -1559,6 +1561,43 @@
 
         verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
                 eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        assertEquals(BrightnessReason.REASON_OFFLOAD, mHolder.dpc.mBrightnessReason.getReason());
+    }
+
+    @Test
+    public void testBrightness_AutomaticHigherPriorityThanOffload() {
+        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        float brightness = 0.34f;
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        mHolder.dpc.setBrightnessFromOffload(brightness);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        assertEquals(BrightnessReason.REASON_OFFLOAD, mHolder.dpc.mBrightnessReason.getReason());
+
+        // Now automatic brightness becomes available
+        brightness = 0.22f;
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(brightness);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        assertEquals(BrightnessReason.REASON_AUTOMATIC, mHolder.dpc.mBrightnessReason.getReason());
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index b99ecf3..14de527 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -46,7 +46,6 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.PowerManager;
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.SurfaceControl;
@@ -1229,8 +1228,6 @@
 
         verify(mDisplayOffloader).stopOffload();
         assertFalse(mDisplayOffloadSession.isActive());
-        verify(mMockedDisplayPowerController).setBrightnessFromOffload(
-                PowerManager.BRIGHTNESS_INVALID_FLOAT);
     }
 
     private void initDisplayOffloadSession() {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index 289d54b..9b6cc0a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -393,7 +393,31 @@
         OffloadBrightnessStrategy offloadBrightnessStrategy = mock(OffloadBrightnessStrategy.class);
         when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(
                 offloadBrightnessStrategy);
-        mDisplayBrightnessController.setBrightnessFromOffload(brightness);
+        boolean brightnessUpdated =
+                mDisplayBrightnessController.setBrightnessFromOffload(brightness);
         verify(offloadBrightnessStrategy).setOffloadScreenBrightness(brightness);
+        assertTrue(brightnessUpdated);
+    }
+
+    @Test
+    public void setBrightnessFromOffload_OffloadStrategyNull() {
+        float brightness = 0.4f;
+        when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(null);
+        boolean brightnessUpdated =
+                mDisplayBrightnessController.setBrightnessFromOffload(brightness);
+        assertFalse(brightnessUpdated);
+    }
+
+    @Test
+    public void setBrightnessFromOffload_BrightnessUnchanged() {
+        float brightness = 0.4f;
+        OffloadBrightnessStrategy offloadBrightnessStrategy = mock(OffloadBrightnessStrategy.class);
+        when(offloadBrightnessStrategy.getOffloadScreenBrightness()).thenReturn(brightness);
+        when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(
+                offloadBrightnessStrategy);
+        boolean brightnessUpdated =
+                mDisplayBrightnessController.setBrightnessFromOffload(brightness);
+        verify(offloadBrightnessStrategy, never()).setOffloadScreenBrightness(brightness);
+        assertFalse(brightnessUpdated);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
index 1c681ce..0e89d83 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
@@ -247,6 +247,7 @@
         displayPowerRequest.screenBrightnessOverride = Float.NaN;
         when(mFollowerBrightnessStrategy.getBrightnessToFollow()).thenReturn(Float.NaN);
         when(mTemporaryBrightnessStrategy.getTemporaryScreenBrightness()).thenReturn(Float.NaN);
+        when(mAutomaticBrightnessStrategy.shouldUseAutoBrightness()).thenReturn(true);
         when(mOffloadBrightnessStrategy.getOffloadScreenBrightness()).thenReturn(0.3f);
         assertEquals(mOffloadBrightnessStrategy, mDisplayBrightnessStrategySelector.selectStrategy(
                 displayPowerRequest, Display.STATE_ON));
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index ba462e3..a5dc668 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -188,29 +188,6 @@
     }
 
     @Test
-    public void testAutoBrightnessState_BrightnessReasonIsOffload() {
-        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
-        int targetDisplayState = Display.STATE_ON;
-        boolean allowAutoBrightnessWhileDozing = false;
-        int brightnessReason = BrightnessReason.REASON_OFFLOAD;
-        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        float lastUserSetBrightness = 0.2f;
-        boolean userSetBrightnessChanged = true;
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
-                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
-                userSetBrightnessChanged);
-        verify(mAutomaticBrightnessController)
-                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                        mBrightnessConfiguration,
-                        lastUserSetBrightness,
-                        userSetBrightnessChanged, 0.5f,
-                        false, policy, true);
-        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
-        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
-    }
-
-    @Test
     public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_DOZE;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index fb47aa8..9d3caa5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -98,8 +98,6 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
-    final BroadcastQueue[] mBroadcastQueues = new BroadcastQueue[1];
-
     @Mock
     AppOpsService mAppOpsService;
     @Mock
@@ -120,6 +118,7 @@
     HandlerThread mHandlerThread;
     TestLooperManager mLooper;
     AtomicInteger mNextPid;
+    BroadcastHistory mEmptyHistory;
 
     /**
      * Map from PID to registered registered runtime receivers.
@@ -137,6 +136,13 @@
                 .acquireLooperManager(mHandlerThread.getLooper()));
         mNextPid = new AtomicInteger(100);
 
+        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+        mEmptyHistory = new BroadcastHistory(mConstants) {
+            public void addBroadcastToHistoryLocked(BroadcastRecord original) {
+                // Ignored
+            }
+        };
+
         LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
         LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
         LocalServices.removeServiceForTest(PackageManagerInternal.class);
@@ -164,8 +170,6 @@
         mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
         doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
         doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
-
-        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
     }
 
     public void tearDown() throws Exception {
@@ -213,8 +217,8 @@
         }
 
         @Override
-        public BroadcastQueue[] getBroadcastQueues(ActivityManagerService service) {
-            return mBroadcastQueues;
+        public BroadcastQueue getBroadcastQueue(ActivityManagerService service) {
+            return null;
         }
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index cc6fc80..bcf297f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -118,15 +118,9 @@
         mConstants.DELAY_NORMAL_MILLIS = 10_000;
         mConstants.DELAY_CACHED_MILLIS = 120_000;
 
-        final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
-            public void addBroadcastToHistoryLocked(BroadcastRecord original) {
-                // Ignored
-            }
-        };
-
         mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
-            mConstants, mConstants, mSkipPolicy, emptyHistory);
-        mBroadcastQueues[0] = mImpl;
+                mConstants, mConstants, mSkipPolicy, mEmptyHistory);
+        mAms.setBroadcastQueueForTest(mImpl);
 
         doReturn(1L).when(mQueue1).getRunnableAt();
         doReturn(2L).when(mQueue2).getRunnableAt();
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 3f6117b..56e5bd6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -85,12 +85,8 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.After;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 import org.mockito.ArgumentMatcher;
 import org.mockito.InOrder;
 import org.mockito.verification.VerificationMode;
@@ -100,7 +96,6 @@
 import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -114,18 +109,10 @@
  * Common tests for {@link BroadcastQueue} implementations.
  */
 @MediumTest
-@RunWith(Parameterized.class)
 @SuppressWarnings("GuardedBy")
 public class BroadcastQueueTest extends BaseBroadcastQueueTest {
     private static final String TAG = "BroadcastQueueTest";
 
-    private final Impl mImpl;
-
-    private enum Impl {
-        DEFAULT,
-        MODERN,
-    }
-
     private BroadcastQueue mQueue;
     private UidObserver mUidObserver;
 
@@ -157,15 +144,6 @@
      */
     private List<Pair<Integer, String>> mScheduledBroadcasts = new ArrayList<>();
 
-    @Parameters(name = "impl={0}")
-    public static Collection<Object[]> data() {
-        return Arrays.asList(new Object[][] { {Impl.DEFAULT}, {Impl.MODERN} });
-    }
-
-    public BroadcastQueueTest(Impl impl) {
-        mImpl = impl;
-    }
-
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -251,24 +229,9 @@
         }).when(mAms).registerUidObserver(any(), anyInt(),
                 eq(ActivityManager.PROCESS_STATE_TOP), any());
 
-        final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
-            public void addBroadcastToHistoryLocked(BroadcastRecord original) {
-                // Ignored
-            }
-        };
-
-        if (mImpl == Impl.DEFAULT) {
-            mQueue = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG,
-                    mConstants, mSkipPolicy, emptyHistory, false,
-                    ProcessList.SCHED_GROUP_DEFAULT);
-        } else if (mImpl == Impl.MODERN) {
-            mQueue = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
-                    mConstants, mConstants, mSkipPolicy, emptyHistory);
-        } else {
-            throw new UnsupportedOperationException();
-        }
-        mBroadcastQueues[0] = mQueue;
-
+        mQueue = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
+                mConstants, mConstants, mSkipPolicy, mEmptyHistory);
+        mAms.setBroadcastQueueForTest(mQueue);
         mQueue.start(mContext.getContentResolver());
 
         // Set the constants after invoking BroadcastQueue.start() to ensure they don't
@@ -489,10 +452,8 @@
     }
 
     private void assertHealth() {
-        if (mImpl == Impl.MODERN) {
-            // If this fails, it'll throw a clear reason message
-            ((BroadcastQueueModernImpl) mQueue).assertHealthLocked();
-        }
+        // If this fails, it'll throw a clear reason message
+        ((BroadcastQueueModernImpl) mQueue).assertHealthLocked();
     }
 
     private static Map<String, Object> asMap(Bundle bundle) {
@@ -814,18 +775,13 @@
                     .setReportedProcState(ActivityManager.PROCESS_STATE_RECEIVER);
             verify(mAms, times(2)).enqueueOomAdjTargetLocked(eq(receiverApp));
 
-            if ((mImpl == Impl.DEFAULT) && (receiverApp == receiverBlueApp)) {
-                // Nuance: the default implementation doesn't ask for manifest
-                // cold-started apps to be thawed, but the modern stack does
-            } else {
-                // Confirm that app was thawed
-                verify(mAms.mOomAdjuster, atLeastOnce()).unfreezeTemporarily(
-                        eq(receiverApp), eq(OOM_ADJ_REASON_START_RECEIVER));
+            // Confirm that app was thawed
+            verify(mAms.mOomAdjuster, atLeastOnce()).unfreezeTemporarily(
+                    eq(receiverApp), eq(OOM_ADJ_REASON_START_RECEIVER));
 
-                // Confirm that we added package to process
-                verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName),
-                        anyLong(), any());
-            }
+            // Confirm that we added package to process
+            verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName),
+                    anyLong(), any());
 
             // Confirm that we've reported package as being used
             verify(mAms, atLeastOnce()).notifyPackageUse(eq(receiverApp.info.packageName),
@@ -868,9 +824,6 @@
      */
     @Test
     public void testWedged_Registered_Ordered() throws Exception {
-        // Legacy stack doesn't detect these ANRs; likely an oversight
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN,
                 ProcessBehavior.WEDGE);
@@ -891,9 +844,6 @@
      */
     @Test
     public void testWedged_Registered_ResultTo() throws Exception {
-        // Legacy stack doesn't detect these ANRs; likely an oversight
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN,
                 ProcessBehavior.WEDGE);
@@ -994,9 +944,7 @@
                 getUidForPackage(PACKAGE_GREEN));
         // Modern queue always kills the target process when broadcast delivery fails, where as
         // the legacy queue leaves the process killing task to AMS
-        if (mImpl == Impl.MODERN) {
-            assertNull(receiverGreenApp);
-        }
+        assertNull(receiverGreenApp);
         final ProcessRecord receiverBlueApp = mAms.getProcessRecordLocked(PACKAGE_BLUE,
                 getUidForPackage(PACKAGE_BLUE));
         verifyScheduleReceiver(receiverBlueApp, airplane);
@@ -1110,10 +1058,8 @@
         waitForIdle();
         // Legacy stack does not remove registered receivers as part of
         // cleanUpDisabledPackageReceiversLocked() call, so verify this only on modern queue.
-        if (mImpl == Impl.MODERN) {
-            verifyScheduleReceiver(never(), callerApp, USER_GUEST);
-            verifyScheduleRegisteredReceiver(never(), callerApp, USER_GUEST);
-        }
+        verifyScheduleReceiver(never(), callerApp, USER_GUEST);
+        verifyScheduleRegisteredReceiver(never(), callerApp, USER_GUEST);
         for (String pkg : new String[] {
                 PACKAGE_GREEN, PACKAGE_BLUE, PACKAGE_YELLOW
         }) {
@@ -1199,9 +1145,6 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_AVOID_REPEATED_BCAST_RE_ENQUEUES)
     public void testRepeatedKillWithoutNotify() throws Exception {
-        // Legacy queue does not handle repeated kills that don't get notified.
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
 
@@ -1227,9 +1170,7 @@
 
         // Modern queue always kills the target process when broadcast delivery fails, where as
         // the legacy queue leaves the process killing task to AMS
-        if (mImpl == Impl.MODERN) {
-            assertNull(receiverGreenApp);
-        }
+        assertNull(receiverGreenApp);
         verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
         verifyScheduleReceiver(times(1), receiverYellowApp, airplane);
         verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
@@ -1273,11 +1214,7 @@
         assertNotEquals(receiverBlueApp, restartedReceiverBlueApp);
         // Legacy queue will always try delivering the broadcast even if the process
         // has been killed.
-        if (mImpl == Impl.MODERN) {
-            verifyScheduleReceiver(never(), receiverBlueApp, airplane);
-        } else {
-            verifyScheduleReceiver(times(1), receiverBlueApp, airplane);
-        }
+        verifyScheduleReceiver(never(), receiverBlueApp, airplane);
         // Verify that the new process receives the broadcast.
         verifyScheduleReceiver(times(1), restartedReceiverBlueApp, airplane);
     }
@@ -1671,9 +1608,6 @@
      */
     @Test
     public void testPrioritized_withDeferrableBroadcasts() throws Exception {
-        // Legacy stack doesn't support deferral
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
@@ -1834,10 +1768,6 @@
 
     @Test
     public void testReplacePending_withUrgentBroadcast() throws Exception {
-        // The behavior is same with the legacy queue but AMS takes care of finding
-        // the right queue and replacing the broadcast.
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
 
         final Intent timeTickFirst = new Intent(Intent.ACTION_TIME_TICK);
@@ -1903,15 +1833,9 @@
 
         waitForIdle();
 
-        if (mImpl == Impl.MODERN) {
-            verifyScheduleRegisteredReceiver(times(2), receiverGreenApp, airplane);
-            verifyScheduleRegisteredReceiver(times(2), receiverBlueApp, airplane);
-            verifyScheduleRegisteredReceiver(times(1), receiverYellowApp, airplane);
-        } else {
-            verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, airplane);
-            verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
-            verifyScheduleRegisteredReceiver(never(), receiverYellowApp, airplane);
-        }
+        verifyScheduleRegisteredReceiver(times(2), receiverGreenApp, airplane);
+        verifyScheduleRegisteredReceiver(times(2), receiverBlueApp, airplane);
+        verifyScheduleRegisteredReceiver(times(1), receiverYellowApp, airplane);
     }
 
     @Test
@@ -1931,14 +1855,10 @@
                 withPriority(receiverGreenA, 5))));
 
         waitForIdle();
-        if (mImpl == Impl.DEFAULT) {
-            verifyScheduleRegisteredReceiver(times(2), receiverGreenApp, airplane);
-        } else {
-            // In the modern queue, we don't end up replacing the old broadcast to
-            // avoid creating priority inversion and so the process will receive
-            // both the old and new broadcasts.
-            verifyScheduleRegisteredReceiver(times(3), receiverGreenApp, airplane);
-        }
+        // In the modern queue, we don't end up replacing the old broadcast to
+        // avoid creating priority inversion and so the process will receive
+        // both the old and new broadcasts.
+        verifyScheduleRegisteredReceiver(times(3), receiverGreenApp, airplane);
     }
 
     @Test
@@ -1966,11 +1886,7 @@
 
         verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, timeTick);
         verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, timeTick);
-        if (mImpl == Impl.MODERN) {
-            verifyScheduleRegisteredReceiver(times(2), receiverGreenApp, airplane);
-        } else {
-            verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, airplane);
-        }
+        verifyScheduleRegisteredReceiver(times(2), receiverGreenApp, airplane);
         verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
     }
 
@@ -2001,6 +1917,56 @@
     }
 
     @Test
+    public void testReplacePendingToCachedProcess_withDeferrableBroadcast() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
+
+        setProcessFreezable(receiverGreenApp, true, false);
+        mQueue.onProcessFreezableChangedLocked(receiverGreenApp);
+        waitForIdle();
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK)
+                .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        final BroadcastOptions opts = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE);
+
+        final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 10);
+        final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 5);
+        final BroadcastFilter receiverYellow = makeRegisteredReceiver(receiverYellowApp, 0);
+        enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, opts, List.of(
+                receiverGreen, receiverBlue, receiverYellow)));
+
+        // Enqueue the broadcast again to replace the earlier one
+        enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, opts, List.of(
+                receiverGreen, receiverBlue, receiverYellow)));
+
+        waitForIdle();
+        // Green should still be in the cached state and shouldn't receive the broadcast
+        verifyScheduleRegisteredReceiver(never(), receiverGreenApp, timeTick);
+
+        final IApplicationThread blueThread = receiverBlueApp.getThread();
+        final IApplicationThread yellowThread = receiverYellowApp.getThread();
+        final InOrder inOrder = inOrder(blueThread, yellowThread);
+        inOrder.verify(blueThread).scheduleRegisteredReceiver(
+                any(), argThat(filterEqualsIgnoringComponent(timeTick)),
+                anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(),
+                eq(UserHandle.USER_SYSTEM), anyInt(), anyInt(), any());
+        inOrder.verify(yellowThread).scheduleRegisteredReceiver(
+                any(), argThat(filterEqualsIgnoringComponent(timeTick)),
+                anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(),
+                eq(UserHandle.USER_SYSTEM), anyInt(), anyInt(), any());
+
+        setProcessFreezable(receiverGreenApp, false, false);
+        mQueue.onProcessFreezableChangedLocked(receiverGreenApp);
+        waitForIdle();
+
+        // Confirm that green receives the broadcast once it comes out of the cached state
+        verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, timeTick);
+    }
+
+    @Test
     public void testIdleAndBarrier() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
@@ -2171,16 +2137,9 @@
         }
         waitForIdle();
 
-        final int expectedTimes;
-        switch (mImpl) {
-            // Original stack requested for every single receiver; yikes
-            case DEFAULT: expectedTimes = 64; break;
-            // Modern stack requests once each time we promote a process to
-            // running; we promote "green" twice, and "blue" and "yellow" once
-            case MODERN: expectedTimes = 4; break;
-            default: throw new UnsupportedOperationException();
-        }
-
+        // Modern stack requests once each time we promote a process to
+        // running; we promote "green" twice, and "blue" and "yellow" once
+        final int expectedTimes = 4;
         verify(mAms, times(expectedTimes))
                 .updateOomAdjPendingTargetsLocked(eq(OOM_ADJ_REASON_START_RECEIVER));
     }
@@ -2249,9 +2208,6 @@
      */
     @Test
     public void testDeferralPolicy_UntilActive() throws Exception {
-        // Legacy stack doesn't support deferral
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
@@ -2297,9 +2253,6 @@
      */
     @Test
     public void testDeferralPolicy_UntilActive_WithMultiProcessUid() throws Exception {
-        // Legacy stack doesn't support deferral
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverGreenApp1 = makeActiveProcessRecord(PACKAGE_GREEN);
         final ProcessRecord receiverGreenApp2 = makeActiveProcessRecord(PACKAGE_GREEN,
@@ -2331,9 +2284,6 @@
 
     @Test
     public void testBroadcastDelivery_uidForeground() throws Exception {
-        // Legacy stack doesn't support prioritization to foreground app.
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
@@ -2365,9 +2315,6 @@
 
     @Test
     public void testPrioritizedBroadcastDelivery_uidForeground() throws Exception {
-        // Legacy stack doesn't support prioritization to foreground app.
-        Assume.assumeTrue(mImpl == Impl.MODERN);
-
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
index f0efb79..878b945 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -61,8 +61,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.server.am.BroadcastDispatcher.DeferredBootCompletedBroadcastPerUser;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -747,68 +745,6 @@
         }
     }
 
-    /**
-     * Test the class {@link BroadcastDispatcher#DeferredBootCompletedBroadcastPerUser}
-     */
-    @Test
-    public void testDeferBootCompletedBroadcast_dispatcher() {
-        testDeferBootCompletedBroadcast_dispatcher_internal(ACTION_LOCKED_BOOT_COMPLETED, false);
-        testDeferBootCompletedBroadcast_dispatcher_internal(ACTION_BOOT_COMPLETED, false);
-        testDeferBootCompletedBroadcast_dispatcher_internal(ACTION_LOCKED_BOOT_COMPLETED, true);
-        testDeferBootCompletedBroadcast_dispatcher_internal(ACTION_BOOT_COMPLETED, true);
-    }
-
-    private void testDeferBootCompletedBroadcast_dispatcher_internal(String action,
-            boolean isAllUidReady) {
-        final List<ResolveInfo> receivers = createReceiverInfos(PACKAGE_LIST, new int[] {USER0});
-        final BroadcastRecord br = createBroadcastRecord(receivers, USER0, new Intent(action));
-        assertEquals(PACKAGE_LIST.length, br.receivers.size());
-
-        SparseArray<BroadcastRecord> deferred = br.splitDeferredBootCompletedBroadcastLocked(
-                mActivityManagerInternal, DEFER_BOOT_COMPLETED_BROADCAST_ALL);
-        // original BroadcastRecord receivers list is empty now.
-        assertTrue(br.receivers.isEmpty());
-        assertEquals(PACKAGE_LIST.length, deferred.size());
-
-        DeferredBootCompletedBroadcastPerUser deferredPerUser =
-                new DeferredBootCompletedBroadcastPerUser(USER0);
-        deferredPerUser.enqueueBootCompletedBroadcasts(action, deferred);
-
-        if (action.equals(ACTION_LOCKED_BOOT_COMPLETED)) {
-            assertEquals(PACKAGE_LIST.length,
-                    deferredPerUser.mDeferredLockedBootCompletedBroadcasts.size());
-            assertTrue(deferredPerUser.mLockedBootCompletedBroadcastReceived);
-            for (int i = 0; i < PACKAGE_LIST.length; i++) {
-                final int uid = UserHandle.getUid(USER0, getAppId(i));
-                if (!isAllUidReady) {
-                    deferredPerUser.updateUidReady(uid);
-                }
-                BroadcastRecord d = deferredPerUser.dequeueDeferredBootCompletedBroadcast(
-                        isAllUidReady);
-                final ResolveInfo info = (ResolveInfo) d.receivers.get(0);
-                assertEquals(PACKAGE_LIST[i], info.activityInfo.applicationInfo.packageName);
-                assertEquals(uid, info.activityInfo.applicationInfo.uid);
-            }
-            assertEquals(0, deferredPerUser.mUidReadyForLockedBootCompletedBroadcast.size());
-        } else if (action.equals(ACTION_BOOT_COMPLETED)) {
-            assertEquals(PACKAGE_LIST.length,
-                    deferredPerUser.mDeferredBootCompletedBroadcasts.size());
-            assertTrue(deferredPerUser.mBootCompletedBroadcastReceived);
-            for (int i = 0; i < PACKAGE_LIST.length; i++) {
-                final int uid = UserHandle.getUid(USER0, getAppId(i));
-                if (!isAllUidReady) {
-                    deferredPerUser.updateUidReady(uid);
-                }
-                BroadcastRecord d = deferredPerUser.dequeueDeferredBootCompletedBroadcast(
-                        isAllUidReady);
-                final ResolveInfo info = (ResolveInfo) d.receivers.get(0);
-                assertEquals(PACKAGE_LIST[i], info.activityInfo.applicationInfo.packageName);
-                assertEquals(uid, info.activityInfo.applicationInfo.uid);
-            }
-            assertEquals(0, deferredPerUser.mUidReadyForBootCompletedBroadcast.size());
-        }
-    }
-
     private static void cleanupDisabledPackageReceivers(BroadcastRecord record,
             String packageName, int userId) {
         record.cleanupDisabledPackageReceiversLocked(packageName, null /* filterByClasses */,
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 1226f0c..872ac40 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -297,6 +297,10 @@
         } else {
             updateProcessRecordNodes(Arrays.asList(apps));
             if (apps.length == 1) {
+                final ProcessRecord app = apps[0];
+                if (!sService.mProcessList.getLruProcessesLOSP().contains(app)) {
+                    sService.mProcessList.getLruProcessesLOSP().add(app);
+                }
                 sService.mOomAdjuster.updateOomAdjLocked(apps[0], OOM_ADJ_REASON_NONE);
             } else {
                 setProcessesToLru(apps);
@@ -475,7 +479,16 @@
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
         updateOomAdj(app);
 
-        assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, CACHED_APP_MIN_ADJ,
+        final int expectedAdj;
+        if (sService.mConstants.ENABLE_NEW_OOMADJ) {
+            // A cached empty process can be at best a level higher than the min cached adj.
+            expectedAdj = sFirstCachedAdj;
+        } else {
+            // This is wrong but legacy behavior is going to be removed and not worth fixing.
+            expectedAdj = CACHED_APP_MIN_ADJ;
+        }
+
+        assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, expectedAdj,
                 SCHED_GROUP_BACKGROUND);
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 6bcd778..c6a6865 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -60,10 +60,13 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.job.JobInfo;
@@ -71,12 +74,15 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
 import android.net.NetworkRequest;
 import android.os.Looper;
 import android.os.PowerManager;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccSlotMapping;
 import android.util.ArraySet;
 import android.util.EmptyArray;
 import android.util.SparseArray;
@@ -104,6 +110,9 @@
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 public class FlexibilityControllerTest {
@@ -113,6 +122,9 @@
 
     private MockitoSession mMockingSession;
     private BroadcastReceiver mBroadcastReceiver;
+    private final SparseArray<ArraySet<String>> mCarrierPrivilegedApps = new SparseArray<>();
+    private final SparseArray<TelephonyManager.CarrierPrivilegesCallback>
+            mCarrierPrivilegedCallbacks = new SparseArray<>();
     private FlexibilityController mFlexibilityController;
     private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
     private JobStore mJobStore;
@@ -130,6 +142,10 @@
     @Mock
     private PrefetchController mPrefetchController;
     @Mock
+    private TelephonyManager mTelephonyManager;
+    @Mock
+    private IPackageManager mIPackageManager;
+    @Mock
     private PackageManager mPackageManager;
 
     @Before
@@ -138,6 +154,7 @@
                 .initMocks(this)
                 .strictness(Strictness.LENIENT)
                 .spyStatic(DeviceConfig.class)
+                .mockStatic(AppGlobals.class)
                 .mockStatic(LocalServices.class)
                 .startMocking();
         // Called in StateController constructor.
@@ -167,17 +184,23 @@
                         -> mDeviceConfigPropertiesBuilder.build())
                 .when(() -> DeviceConfig.getProperties(
                         eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any()));
+        // Used in FlexibilityController.SpecialAppTracker.
+        when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION))
+                .thenReturn(true);
         //used to get jobs by UID
         mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir());
         doReturn(mJobStore).when(mJobSchedulerService).getJobStore();
         // Used in JobStatus.
-        doReturn(mock(PackageManagerInternal.class))
-                .when(() -> LocalServices.getService(PackageManagerInternal.class));
+        doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
         // Freeze the clocks at a moment in time
         JobSchedulerService.sSystemClock =
                 Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
+        // Set empty set of privileged apps.
+        setSimSlotMappings(null);
+        setPowerWhitelistExceptIdle();
         // Initialize real objects.
         doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(any());
         ArgumentCaptor<BroadcastReceiver> receiverCaptor =
@@ -249,9 +272,13 @@
     }
 
     private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
+        return createJobStatus(testTag, job, SOURCE_PACKAGE);
+    }
+
+    private JobStatus createJobStatus(String testTag, JobInfo.Builder job, String sourcePackage) {
         JobInfo jobInfo = job.build();
         JobStatus js = JobStatus.createFromJobInfo(
-                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
+                jobInfo, 1000, sourcePackage, SOURCE_USER_ID, "FCTest", testTag);
         js.enqueueTime = FROZEN_TIME;
         js.setStandbyBucket(ACTIVE_INDEX);
         if (js.hasFlexibilityConstraint()) {
@@ -1084,7 +1111,6 @@
 
     @Test
     public void testAllowlistedAppBypass() {
-        setPowerWhitelistExceptIdle();
         mFlexibilityController.onSystemServicesReady();
 
         JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
@@ -1118,6 +1144,148 @@
     }
 
     @Test
+    public void testCarrierPrivilegedAppBypass() throws Exception {
+        mFlexibilityController.onSystemServicesReady();
+
+        final String carrier1Pkg1 = "com.test.carrier.1.pkg.1";
+        final String carrier1Pkg2 = "com.test.carrier.1.pkg.2";
+        final String carrier2Pkg = "com.test.carrier.2.pkg";
+        final String nonCarrierPkg = "com.test.normal.pkg";
+
+        setPackageUid(carrier1Pkg1, 1);
+        setPackageUid(carrier1Pkg2, 11);
+        setPackageUid(carrier2Pkg, 2);
+        setPackageUid(nonCarrierPkg, 3);
+
+        // Set the second carrier's privileged list before SIM configuration is sent to test
+        // initialization.
+        setCarrierPrivilegedAppList(2, carrier2Pkg);
+
+        UiccSlotMapping sim1 = mock(UiccSlotMapping.class);
+        UiccSlotMapping sim2 = mock(UiccSlotMapping.class);
+        doReturn(1).when(sim1).getLogicalSlotIndex();
+        doReturn(2).when(sim2).getLogicalSlotIndex();
+        setSimSlotMappings(List.of(sim1, sim2));
+
+        JobStatus jsHighC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier1Pkg1);
+        JobStatus jsDefaultC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier1Pkg1);
+        JobStatus jsLowC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier1Pkg1);
+        JobStatus jsMinC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier1Pkg1);
+        JobStatus jsHighC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier1Pkg2);
+        JobStatus jsDefaultC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier1Pkg2);
+        JobStatus jsLowC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier1Pkg2);
+        JobStatus jsMinC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier1Pkg2);
+        JobStatus jsHighC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier2Pkg);
+        JobStatus jsDefaultC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier2Pkg);
+        JobStatus jsLowC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier2Pkg);
+        JobStatus jsMinC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier2Pkg);
+        JobStatus jsHighNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), nonCarrierPkg);
+        JobStatus jsDefaultNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), nonCarrierPkg);
+        JobStatus jsLowNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), nonCarrierPkg);
+        JobStatus jsMinNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), nonCarrierPkg);
+
+        setCarrierPrivilegedAppList(1);
+        synchronized (mFlexibilityController.mLock) {
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+
+        // Only mark the first package of carrier 1 as privileged. Only that app's jobs should
+        // be exempted.
+        setCarrierPrivilegedAppList(1, carrier1Pkg1);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+
+        // Add the second package of carrier 1. Both apps' jobs should be exempted.
+        setCarrierPrivilegedAppList(1, carrier1Pkg1, carrier1Pkg2);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+
+        // Remove a SIM slot. The relevant app's should no longer have exempted jobs.
+        setSimSlotMappings(List.of(sim1));
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+    }
+
+    @Test
     public void testForegroundAppBypass() {
         JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
                 createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
@@ -1753,6 +1921,24 @@
         }
     }
 
+    private void setCarrierPrivilegedAppList(int logicalIndex, String... packages) {
+        final ArraySet<String> packageSet = packages == null
+                ? new ArraySet<>() : new ArraySet<>(packages);
+        mCarrierPrivilegedApps.put(logicalIndex, packageSet);
+
+        TelephonyManager.CarrierPrivilegesCallback callback =
+                mCarrierPrivilegedCallbacks.get(logicalIndex);
+        if (callback != null) {
+            callback.onCarrierPrivilegesChanged(packageSet, Collections.emptySet());
+            waitForQuietModuleThread();
+        }
+    }
+
+    private void setPackageUid(final String pkgName, final int uid) throws Exception {
+        doReturn(uid).when(mIPackageManager)
+                .getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid)));
+    }
+
     private void setPowerWhitelistExceptIdle(String... packages) {
         doReturn(packages == null ? EmptyArray.STRING : packages)
                 .when(mDeviceIdleInternal).getFullPowerWhitelistExceptIdle();
@@ -1763,6 +1949,47 @@
         }
     }
 
+    private void setSimSlotMappings(@Nullable Collection<UiccSlotMapping> simSlotMapping) {
+        clearInvocations(mTelephonyManager);
+        final Collection<UiccSlotMapping> returnedMapping = simSlotMapping == null
+                ? Collections.emptyList() : simSlotMapping;
+        doReturn(returnedMapping).when(mTelephonyManager).getSimSlotMapping();
+        if (mBroadcastReceiver != null) {
+            final Intent intent = new Intent(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
+            mBroadcastReceiver.onReceive(mContext, intent);
+            waitForQuietModuleThread();
+        }
+        if (returnedMapping.size() > 0) {
+            ArgumentCaptor<TelephonyManager.CarrierPrivilegesCallback> callbackCaptor =
+                    ArgumentCaptor.forClass(TelephonyManager.CarrierPrivilegesCallback.class);
+            ArgumentCaptor<Integer> logicalIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+
+            final int minExpectedNewRegistrations = Math.max(0,
+                    returnedMapping.size() - mCarrierPrivilegedCallbacks.size());
+            verify(mTelephonyManager, atLeast(minExpectedNewRegistrations))
+                    .registerCarrierPrivilegesCallback(
+                            logicalIndexCaptor.capture(), any(), callbackCaptor.capture());
+
+            final List<Integer> registeredIndices = logicalIndexCaptor.getAllValues();
+            final List<TelephonyManager.CarrierPrivilegesCallback> registeredCallbacks =
+                    callbackCaptor.getAllValues();
+            for (int i = 0; i < registeredIndices.size(); ++i) {
+                final int logicalIndex = registeredIndices.get(i);
+                final TelephonyManager.CarrierPrivilegesCallback callback =
+                        registeredCallbacks.get(i);
+
+                mCarrierPrivilegedCallbacks.put(logicalIndex, callback);
+
+                // The API contract promises a callback upon registration with the current list.
+                final ArraySet<String> cpApps = mCarrierPrivilegedApps.get(logicalIndex);
+                callback.onCarrierPrivilegesChanged(
+                        cpApps == null ? Collections.emptySet() : cpApps,
+                        Collections.emptySet());
+            }
+            waitForQuietModuleThread();
+        }
+    }
+
     private void setUidBias(int uid, int bias) {
         int prevBias = mJobSchedulerService.getUidBias(uid);
         doReturn(bias).when(mJobSchedulerService).getUidBias(uid);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 7bbcd50..bc7c9a5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.server.pm;
 
+import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE;
+import static android.content.pm.PackageManager.FEATURE_EMBEDDED;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
 import static android.os.UserManager.DISALLOW_OUTGOING_CALLS;
 import static android.os.UserManager.DISALLOW_SMS;
 import static android.os.UserManager.DISALLOW_USER_SWITCH;
@@ -41,6 +45,7 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.KeyguardManager;
 import android.content.Context;
@@ -48,6 +53,7 @@
 import android.content.pm.UserInfo;
 import android.multiuser.Flags;
 import android.os.PowerManager;
+import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -76,6 +82,7 @@
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -120,11 +127,14 @@
 
     private static final String TAG_RESTRICTIONS = "restrictions";
 
+    private static final String PRIVATE_PROFILE_NAME = "TestPrivateProfile";
+
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .spyStatic(UserManager.class)
             .spyStatic(LocalServices.class)
             .spyStatic(SystemProperties.class)
+            .spyStatic(ActivityManager.class)
             .mockStatic(Settings.Global.class)
             .mockStatic(Settings.Secure.class)
             .build();
@@ -163,6 +173,7 @@
     @Before
     @UiThreadTest // Needed to initialize main handler
     public void setFixtures() {
+        MockitoAnnotations.initMocks(this);
         mSpiedContext = spy(mRealContext);
 
         // Called when WatchedUserStates is constructed
@@ -172,11 +183,12 @@
         when(mDeviceStorageMonitorInternal.isMemoryLow()).thenReturn(false);
         mockGetLocalService(DeviceStorageMonitorInternal.class, mDeviceStorageMonitorInternal);
         when(mSpiedContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager);
-        when(mSpiedContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
+        doReturn(mKeyguardManager).when(mSpiedContext).getSystemService(KeyguardManager.class);
         when(mSpiedContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager);
         mockGetLocalService(LockSettingsInternal.class, mLockSettingsInternal);
         mockGetLocalService(PackageManagerInternal.class, mPackageManagerInternal);
         doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any());
+        mockIsLowRamDevice(false);
 
         // Must construct UserManagerService in the UiThread
         mTestDir = new File(mRealContext.getDataDir(), "umstest");
@@ -570,9 +582,10 @@
 
     @Test
     public void testAutoLockPrivateProfile() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         UserManagerService mSpiedUms = spy(mUms);
         UserInfo privateProfileUser =
-                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
                         USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
         Mockito.doNothing().when(mSpiedUms).setQuietModeEnabledAsync(
                 eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), any(),
@@ -587,10 +600,11 @@
 
     @Test
     public void testAutoLockOnDeviceLockForPrivateProfile() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         UserManagerService mSpiedUms = spy(mUms);
         UserInfo privateProfileUser =
-                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
                 USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
         mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK);
         Mockito.doNothing().when(mSpiedUms).setQuietModeEnabledAsync(
@@ -606,10 +620,11 @@
 
     @Test
     public void testAutoLockOnDeviceLockForPrivateProfile_keyguardUnlocked() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         UserManagerService mSpiedUms = spy(mUms);
         UserInfo privateProfileUser =
-                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
                 USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
         mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK);
 
@@ -623,10 +638,11 @@
 
     @Test
     public void testAutoLockOnDeviceLockForPrivateProfile_flagDisabled() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         mSetFlagsRule.disableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         UserManagerService mSpiedUms = spy(mUms);
         UserInfo privateProfileUser =
-                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
                 USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
 
         mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(true);
@@ -641,13 +657,14 @@
 
     @Test
     public void testAutoLockAfterInactityForPrivateProfile() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
         UserManagerService mSpiedUms = spy(mUms);
         mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY);
         when(mPowerManager.isInteractive()).thenReturn(false);
 
         UserInfo privateProfileUser =
-                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+                mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
                         USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
         Mockito.doNothing().when(mSpiedUms).scheduleMessageToAutoLockPrivateSpace(
                 eq(privateProfileUser.getUserHandle().getIdentifier()), any(),
@@ -662,6 +679,7 @@
 
     @Test
     public void testSetOrUpdateAutoLockPreference_noPrivateProfile() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
 
         mUms.setOrUpdateAutoLockPreferenceForPrivateProfile(
@@ -675,8 +693,9 @@
 
     @Test
     public void testSetOrUpdateAutoLockPreference() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
         mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE);
-        mUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile",
+        mUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
                         USER_TYPE_PROFILE_PRIVATE, 0, 0, null);
 
         // Set the preference to auto lock on device lock
@@ -733,6 +752,77 @@
         }
     }
 
+    @Test
+    public void testCreatePrivateProfileOnHeadlessSystemUser_shouldAllowCreation() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION);
+        UserManagerService mSpiedUms = spy(mUms);
+        int mainUser = mSpiedUms.getMainUserId();
+        doReturn(true).when(mSpiedUms).isHeadlessSystemUserMode();
+        assertThat(mSpiedUms.canAddPrivateProfile(mainUser)).isTrue();
+        assertThat(mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow(
+                PRIVATE_PROFILE_NAME, USER_TYPE_PROFILE_PRIVATE, 0, mainUser, null)).isNotNull();
+    }
+
+    @Test
+    public void testCreatePrivateProfileOnSecondaryUser_shouldNotAllowCreation() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION);
+        UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0);
+        assertThat(mUms.canAddPrivateProfile(user.id)).isFalse();
+        assertThrows(ServiceSpecificException.class,
+                () -> mUms.createProfileForUserWithThrow(PRIVATE_PROFILE_NAME,
+                        USER_TYPE_PROFILE_PRIVATE, 0, user.id, null));
+    }
+
+    @Test
+    public void testCreatePrivateProfileOnAutoDevices_shouldNotAllowCreation() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION);
+        doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_AUTOMOTIVE), anyInt());
+        int mainUser = mUms.getMainUserId();
+        assertThat(mUms.canAddPrivateProfile(0)).isFalse();
+        assertThrows(ServiceSpecificException.class,
+                () -> mUms.createProfileForUserWithThrow(PRIVATE_PROFILE_NAME,
+                        USER_TYPE_PROFILE_PRIVATE, 0, mainUser, null));
+    }
+
+    @Test
+    public void testCreatePrivateProfileOnTV_shouldNotAllowCreation() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION);
+        doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_LEANBACK), anyInt());
+        int mainUser = mUms.getMainUserId();
+        assertThat(mUms.canAddPrivateProfile(0)).isFalse();
+        assertThrows(ServiceSpecificException.class,
+                () -> mUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
+                        USER_TYPE_PROFILE_PRIVATE, 0, mainUser, null));
+    }
+
+    @Test
+    public void testCreatePrivateProfileOnEmbedded_shouldNotAllowCreation() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION);
+        doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_EMBEDDED), anyInt());
+        int mainUser = mUms.getMainUserId();
+        assertThat(mUms.canAddPrivateProfile(0)).isFalse();
+        assertThrows(ServiceSpecificException.class,
+                () -> mUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
+                        USER_TYPE_PROFILE_PRIVATE, 0, mainUser, null));
+    }
+
+    @Test
+    public void testCreatePrivateProfileOnWatch_shouldNotAllowCreation() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION);
+        doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_WATCH), anyInt());
+        int mainUser = mUms.getMainUserId();
+        assertThat(mUms.canAddPrivateProfile(0)).isFalse();
+        assertThrows(ServiceSpecificException.class,
+                () -> mUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME,
+                        USER_TYPE_PROFILE_PRIVATE, 0, mainUser, null));
+    }
+
     /**
      * Returns true if the user's XML file has Default restrictions
      * @param userId Id of the user.
@@ -800,6 +890,10 @@
                 any(), eq(android.provider.Settings.Global.USER_SWITCHER_ENABLED), anyInt()));
     }
 
+    private void mockIsLowRamDevice(boolean isLowRamDevice) {
+        doReturn(isLowRamDevice).when(ActivityManager::isLowRamDeviceStatic);
+    }
+
     private void mockDeviceDemoMode(boolean enabled) {
         doReturn(enabled ? 1 : 0).when(() -> Settings.Global.getInt(
                 any(), eq(android.provider.Settings.Global.DEVICE_DEMO_MODE), anyInt()));
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp
new file mode 100644
index 0000000..e94b8ad
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp
@@ -0,0 +1,56 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    // 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: "RollbackPackageHealthObserverTests",
+
+    srcs: [
+        "*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.runner",
+        "mockito-target-extended-minus-junit4",
+        "services.core",
+        "truth",
+        "flag-junit",
+    ],
+
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: [
+        "device-tests",
+        "automotive-tests",
+    ],
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidManifest.xml b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidManifest.xml
new file mode 100644
index 0000000..c52dbde
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.server.rollback">
+
+    <uses-sdk android:targetSdkVersion="35" />
+
+    <application android:testOnly="true"
+                 android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.server.rollback"
+        android:label="Frameworks Rollback Package Health Observer test" />
+</manifest>
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidTest.xml b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidTest.xml
new file mode 100644
index 0000000..635183c
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Runs Rollback Package Health Observer Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="RollbackPackageHealthObserverTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="RollbackPackageHealthObserverTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.server.rollback" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING b/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING
index e42bdad..6ac56bf 100644
--- a/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING
@@ -1,7 +1,7 @@
 {
   "postsubmit": [
     {
-      "name": "FrameworksMockingServicesTests",
+      "name": "RollbackPackageHealthObserverTests",
       "options": [
         {
           "include-filter": "com.android.server.rollback"
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
new file mode 100644
index 0000000..7ecc7fd
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
@@ -0,0 +1,640 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+import static android.app.WallpaperManager.PORTRAIT;
+import static android.app.WallpaperManager.SQUARE_LANDSCAPE;
+import static android.app.WallpaperManager.SQUARE_PORTRAIT;
+import static android.app.WallpaperManager.getOrientation;
+import static android.app.WallpaperManager.getRotatedOrientation;
+
+import static com.android.window.flags.Flags.FLAG_MULTI_CROP;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.SparseArray;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Unit tests for the most important helpers of {@link WallpaperCropper}, in particular
+ * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(FLAG_MULTI_CROP)
+public class WallpaperCropperTest {
+
+    @Mock
+    private WallpaperDisplayHelper mWallpaperDisplayHelper;
+    private WallpaperCropper mWallpaperCropper;
+
+    private static final Point PORTRAIT_ONE = new Point(500, 800);
+    private static final Point PORTRAIT_TWO = new Point(400, 1000);
+    private static final Point PORTRAIT_THREE = new Point(2000, 800);
+    private static final Point PORTRAIT_FOUR = new Point(1600, 1000);
+
+    private static final Point SQUARE_PORTRAIT_ONE = new Point(1000, 800);
+    private static final Point SQUARE_LANDSCAPE_ONE = new Point(800, 1000);
+
+    /**
+     * Common device: a single screen of portrait/landscape orientation
+     */
+    private static final List<Point> STANDARD_DISPLAY = List.of(PORTRAIT_ONE);
+
+    /** 1: folded: portrait, unfolded: square with w < h */
+    private static final List<Point> FOLDABLE_ONE = List.of(PORTRAIT_ONE, SQUARE_PORTRAIT_ONE);
+
+    /** 2: folded: portrait, unfolded: square with w > h */
+    private static final List<Point> FOLDABLE_TWO = List.of(PORTRAIT_TWO, SQUARE_LANDSCAPE_ONE);
+
+    /** 3: folded: square with w < h, unfolded: portrait */
+    private static final List<Point> FOLDABLE_THREE = List.of(SQUARE_PORTRAIT_ONE, PORTRAIT_THREE);
+
+    /** 4: folded: square with w > h, unfolded: portrait */
+    private static final List<Point> FOLDABLE_FOUR = List.of(SQUARE_LANDSCAPE_ONE, PORTRAIT_FOUR);
+
+    /**
+     * List of different sets of displays for foldable devices. Foldable devices have two displays:
+     * a folded (smaller) unfolded (larger).
+     */
+    private static final List<List<Point>> ALL_FOLDABLE_DISPLAYS = List.of(
+            FOLDABLE_ONE, FOLDABLE_TWO, FOLDABLE_THREE, FOLDABLE_FOUR);
+
+    private SparseArray<Point> mDisplaySizes = new SparseArray<>();
+    private int mFolded = ORIENTATION_UNKNOWN;
+    private int mFoldedRotated = ORIENTATION_UNKNOWN;
+    private int mUnfolded = ORIENTATION_UNKNOWN;
+    private int mUnfoldedRotated = ORIENTATION_UNKNOWN;
+
+    private static final List<Integer> ALL_MODES = List.of(
+            WallpaperCropper.ADD, WallpaperCropper.REMOVE, WallpaperCropper.BALANCE);
+
+    @Before
+    public void setUp() {
+        initMocks(this);
+        mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper);
+    }
+
+    private void setUpWithDisplays(List<Point> displaySizes) {
+        mDisplaySizes = new SparseArray<>();
+        displaySizes.forEach(size -> {
+            mDisplaySizes.put(getOrientation(size), size);
+            Point rotated = new Point(size.y, size.x);
+            mDisplaySizes.put(getOrientation(rotated), rotated);
+        });
+        when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes);
+        if (displaySizes.size() == 2) {
+            Point largestDisplay = displaySizes.stream().max(
+                    Comparator.comparingInt(p -> p.x * p.y)).get();
+            Point smallestDisplay = displaySizes.stream().min(
+                    Comparator.comparingInt(p -> p.x * p.y)).get();
+            mUnfolded = getOrientation(largestDisplay);
+            mFolded = getOrientation(smallestDisplay);
+            mUnfoldedRotated = getRotatedOrientation(mUnfolded);
+            mFoldedRotated = getRotatedOrientation(mFolded);
+        }
+        doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0)))
+                .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt());
+        doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0)))
+                .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt());
+    }
+
+    private int getFoldedOrientation(int orientation) {
+        if (orientation == ORIENTATION_UNKNOWN) return ORIENTATION_UNKNOWN;
+        if (orientation == mUnfolded) return mFolded;
+        if (orientation == mUnfoldedRotated) return mFoldedRotated;
+        return ORIENTATION_UNKNOWN;
+    }
+
+    private int getUnfoldedOrientation(int orientation) {
+        if (orientation == ORIENTATION_UNKNOWN) return ORIENTATION_UNKNOWN;
+        if (orientation == mFolded) return mUnfolded;
+        if (orientation == mFoldedRotated) return mUnfoldedRotated;
+        return ORIENTATION_UNKNOWN;
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#noParallax} successfully removes the parallax in a simple
+     * case, removing the right or left part depending on the "rtl" argument.
+     */
+    @Test
+    public void testNoParallax_noScale() {
+        Point displaySize = new Point(1000, 1000);
+        Point bitmapSize = new Point(1200, 1000);
+        Point expectedCropSize = new Point(1000, 1000);
+        Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ false))
+                .isEqualTo(leftOf(crop, expectedCropSize));
+        assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ true))
+                .isEqualTo(rightOf(crop, expectedCropSize));
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#noParallax} correctly takes zooming into account.
+     */
+    @Test
+    public void testNoParallax_withScale() {
+        Point displaySize = new Point(1000, 1000);
+        Point bitmapSize = new Point(600, 500);
+        Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        Point expectedCropSize = new Point(500, 500);
+        assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ false))
+                .isEqualTo(leftOf(crop, expectedCropSize));
+        assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ true))
+                .isEqualTo(rightOf(crop, expectedCropSize));
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#noParallax} correctly removes parallax when the image is
+     * cropped, i.e. when the crop rectangle is not the full bitmap.
+     */
+    @Test
+    public void testNoParallax_withScaleAndCrop() {
+        Point displaySize = new Point(1000, 1000);
+        Point bitmapSize = new Point(2000, 2000);
+        Rect crop = new Rect(300, 1000, 900, 1500);
+        Point expectedCropSize = new Point(500, 500);
+        assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ false))
+                .isEqualTo(leftOf(crop, expectedCropSize));
+        assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ true))
+                .isEqualTo(rightOf(crop, expectedCropSize));
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getAdjustedCrop} does nothing when the crop has the same
+     * width/height ratio than the screen.
+     */
+    @Test
+    public void testGetAdjustedCrop_noOp() {
+        Point displaySize = new Point(1000, 1000);
+
+        for (Point bitmapSize: List.of(
+                new Point(1000, 1000),
+                new Point(2000, 2000),
+                new Point(500, 500))) {
+            for (Rect crop: List.of(
+                    new Rect(0, 0, bitmapSize.x, bitmapSize.y),
+                    new Rect(100, 200, bitmapSize.x - 100, bitmapSize.y))) {
+                for (int mode: ALL_MODES) {
+                    for (boolean rtl: List.of(true, false)) {
+                        for (boolean parallax: List.of(true, false)) {
+                            assertThat(WallpaperCropper.getAdjustedCrop(
+                                    crop, bitmapSize, displaySize, parallax, rtl, mode))
+                                    .isEqualTo(crop);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with parallax = true,
+     * does not keep more width than needed for {@link WallpaperCropper#MAX_PARALLAX}.
+     */
+    @Test
+    public void testGetAdjustedCrop_tooMuchParallax() {
+        Point displaySize = new Point(1000, 1000);
+        int tooLargeWidth = (int) (displaySize.x * (1 + 2 * WallpaperCropper.MAX_PARALLAX));
+        Point bitmapSize = new Point(tooLargeWidth, 1000);
+        Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        int expectedWidth = (int) (displaySize.x * (1 + WallpaperCropper.MAX_PARALLAX));
+        Point expectedCropSize = new Point(expectedWidth, 1000);
+        for (int mode: ALL_MODES) {
+            assertThat(WallpaperCropper.getAdjustedCrop(
+                    crop, bitmapSize, displaySize, true, false, mode))
+                    .isEqualTo(leftOf(crop, expectedCropSize));
+            assertThat(WallpaperCropper.getAdjustedCrop(
+                    crop, bitmapSize, displaySize, true, true, mode))
+                    .isEqualTo(rightOf(crop, expectedCropSize));
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with parallax = true,
+     * does not remove parallax if the parallax is below {@link WallpaperCropper#MAX_PARALLAX}.
+     */
+    @Test
+    public void testGetAdjustedCrop_acceptableParallax() {
+        Point displaySize = new Point(1000, 1000);
+        List<Integer> acceptableWidths = List.of(displaySize.x,
+                (int) (displaySize.x * (1 + 0.5 * WallpaperCropper.MAX_PARALLAX)),
+                (int) (displaySize.x * (1 + 0.9 * WallpaperCropper.MAX_PARALLAX)),
+                (int) (displaySize.x * (1 + 1.0 * WallpaperCropper.MAX_PARALLAX)));
+        for (int acceptableWidth: acceptableWidths) {
+            Point bitmapSize = new Point(acceptableWidth, 1000);
+            Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+            for (int mode : ALL_MODES) {
+                for (boolean rtl : List.of(false, true)) {
+                    assertThat(WallpaperCropper.getAdjustedCrop(
+                            crop, bitmapSize, displaySize, true, rtl, mode))
+                            .isEqualTo(crop);
+                }
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with
+     * {@link WallpaperCropper#ADD}, correctly enlarges the crop to match the display dimensions,
+     * and adds content to the crop by an equal amount on both sides when possible.
+     */
+    @Test
+    public void testGetAdjustedCrop_add() {
+        Point displaySize = new Point(1000, 1000);
+        Point bitmapSize = new Point(1000, 1000);
+
+        List<Rect> crops = List.of(
+                new Rect(0, 0, 900, 1000),
+                new Rect(0, 0, 1000, 900),
+                new Rect(0, 0, 400, 500),
+                new Rect(500, 600, 1000, 1000));
+
+        List<Rect> expectedAdjustedCrops = List.of(
+                new Rect(0, 0, 1000, 1000),
+                new Rect(0, 0, 1000, 1000),
+                new Rect(0, 0, 500, 500),
+                new Rect(500, 500, 1000, 1000));
+
+        for (int i = 0; i < crops.size(); i++) {
+            Rect crop = crops.get(i);
+            Rect expectedCrop = expectedAdjustedCrops.get(i);
+            for (boolean rtl: List.of(false, true)) {
+                assertThat(WallpaperCropper.getAdjustedCrop(
+                        crop, bitmapSize, displaySize, false, rtl, WallpaperCropper.ADD))
+                        .isEqualTo(expectedCrop);
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with
+     * {@link WallpaperCropper#REMOVE}, correctly shrinks the crop to match the display dimensions,
+     * and removes content by an equal amount on both sides.
+     */
+    @Test
+    public void testGetAdjustedCrop_remove() {
+        Point displaySize = new Point(1000, 1000);
+        Point bitmapSize = new Point(1500, 1500);
+
+        List<Rect> crops = List.of(
+                new Rect(50, 0, 1150, 1000),
+                new Rect(0, 50, 1000, 1150));
+
+        Point expectedCropSize = new Point(1000, 1000);
+
+        for (Rect crop: crops) {
+            for (boolean rtl : List.of(false, true)) {
+                assertThat(WallpaperCropper.getAdjustedCrop(
+                        crop, bitmapSize, displaySize, false, rtl, WallpaperCropper.REMOVE))
+                        .isEqualTo(centerOf(crop, expectedCropSize));
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with
+     * {@link WallpaperCropper#BALANCE}, gives an adjusted crop with the same center and same number
+     * of pixels when possible.
+     */
+    @Test
+    public void testGetAdjustedCrop_balance() {
+        Point displaySize = new Point(500, 1000);
+        Point transposedDisplaySize = new Point(1000, 500);
+        Point bitmapSize = new Point(1000, 1000);
+
+        List<Rect> crops = List.of(
+                new Rect(0, 250, 1000, 750),
+                new Rect(100, 0, 300, 100));
+
+        List<Rect> expectedAdjustedCrops = List.of(
+                new Rect(250, 0, 750, 1000),
+                new Rect(150, 0, 250, 200));
+
+        for (int i = 0; i < crops.size(); i++) {
+            Rect crop = crops.get(i);
+            Rect expected = expectedAdjustedCrops.get(i);
+            assertThat(WallpaperCropper.getAdjustedCrop(
+                    crop, bitmapSize, displaySize, false, false, WallpaperCropper.BALANCE))
+                    .isEqualTo(expected);
+
+            Rect transposedCrop = new Rect(crop.top, crop.left, crop.bottom, crop.right);
+            Rect expectedTransposed = new Rect(
+                    expected.top, expected.left, expected.bottom, expected.right);
+            assertThat(WallpaperCropper.getAdjustedCrop(transposedCrop, bitmapSize,
+                    transposedDisplaySize, false, false, WallpaperCropper.BALANCE))
+                    .isEqualTo(expectedTransposed);
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop} follows a simple centre-align strategy when
+     * no suggested crops are provided.
+     */
+    @Test
+    public void testGetCrop_noSuggestedCrops_centersWallpaper() {
+        setUpWithDisplays(STANDARD_DISPLAY);
+        Point bitmapSize = new Point(800, 1000);
+        Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        SparseArray<Rect> suggestedCrops = new SparseArray<>();
+
+        List<Point> displaySizes = List.of(
+                new Point(500, 1000),
+                new Point(1000, 500));
+        List<Point> expectedCropSizes = List.of(
+                new Point(500, 1000),
+                new Point(800, 400));
+
+        for (int i = 0; i < displaySizes.size(); i++) {
+            Point displaySize = displaySizes.get(i);
+            Point expectedCropSize = expectedCropSizes.get(i);
+            for (boolean rtl : List.of(false, true)) {
+                assertThat(mWallpaperCropper.getCrop(
+                        displaySize, bitmapSize, suggestedCrops, rtl))
+                        .isEqualTo(centerOf(bitmapRect, expectedCropSize));
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop} reuses a suggested crop of the same orientation
+     * as the display if possible, and does not remove additional width for parallax,
+     * but adds width if necessary.
+     */
+    @Test
+    public void testGetCrop_hasSuggestedCrop() {
+        setUpWithDisplays(STANDARD_DISPLAY);
+        Point bitmapSize = new Point(800, 1000);
+        SparseArray<Rect> suggestedCrops = new SparseArray<>();
+        suggestedCrops.put(PORTRAIT, new Rect(0, 0, 400, 800));
+        for (int otherOrientation: List.of(LANDSCAPE, SQUARE_LANDSCAPE, SQUARE_PORTRAIT)) {
+            suggestedCrops.put(otherOrientation, new Rect(0, 0, 10, 10));
+        }
+
+        for (boolean rtl : List.of(false, true)) {
+            assertThat(mWallpaperCropper.getCrop(
+                    new Point(300, 800), bitmapSize, suggestedCrops, rtl))
+                    .isEqualTo(suggestedCrops.get(PORTRAIT));
+            assertThat(mWallpaperCropper.getCrop(
+                    new Point(500, 800), bitmapSize, suggestedCrops, rtl))
+                    .isEqualTo(new Rect(0, 0, 500, 800));
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop}, if there is no suggested crop of the same
+     * orientation as the display, reuses a suggested crop of the rotated orientation if possible,
+     * and preserves the center and number of pixels of the crop if possible.
+     * <p>
+     * To simplify, in this test case all crops have the same size as the display (no zoom)
+     * and are at the center of the image. Also the image is large enough to preserver the number
+     * of pixels (no additional zoom required).
+     */
+    @Test
+    public void testGetCrop_hasRotatedSuggestedCrop() {
+        setUpWithDisplays(STANDARD_DISPLAY);
+        Point bitmapSize = new Point(2000, 1800);
+        Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        SparseArray<Rect> suggestedCrops = new SparseArray<>();
+        Point portrait = PORTRAIT_ONE;
+        Point landscape = new Point(PORTRAIT_ONE.y, PORTRAIT_ONE.x);
+        Point squarePortrait = SQUARE_PORTRAIT_ONE;
+        Point squareLandscape = new Point(SQUARE_PORTRAIT_ONE.y, SQUARE_PORTRAIT_ONE.y);
+        suggestedCrops.put(PORTRAIT, centerOf(bitmapRect, portrait));
+        suggestedCrops.put(SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape));
+        for (boolean rtl : List.of(false, true)) {
+            assertThat(mWallpaperCropper.getCrop(
+                    landscape, bitmapSize, suggestedCrops, rtl))
+                    .isEqualTo(centerOf(bitmapRect, landscape));
+
+            assertThat(mWallpaperCropper.getCrop(
+                    squarePortrait, bitmapSize, suggestedCrops, rtl))
+                    .isEqualTo(centerOf(bitmapRect, squarePortrait));
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop}, when asked for a folded crop with a suggested
+     * crop only for the relative unfolded orientation, creates the folded crop at the center of the
+     * unfolded crop, by removing content on two sides to match the folded screen dimensions.
+     * <p>
+     * To simplify, in this test case all crops have the same size as the display (no zoom)
+     * and are at the center of the image.
+     */
+    @Test
+    public void testGetCrop_hasUnfoldedSuggestedCrop() {
+        for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
+            setUpWithDisplays(displaySizes);
+            Point bitmapSize = new Point(2000, 2400);
+            Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+
+            Point largestDisplay = displaySizes.stream().max(
+                    Comparator.comparingInt(a -> a.x * a.y)).orElseThrow();
+            int unfoldedOne = getOrientation(largestDisplay);
+            int unfoldedTwo = getRotatedOrientation(unfoldedOne);
+            Rect unfoldedCropOne = centerOf(bitmapRect, mDisplaySizes.get(unfoldedOne));
+            Rect unfoldedCropTwo = centerOf(bitmapRect, mDisplaySizes.get(unfoldedTwo));
+            SparseArray<Rect> suggestedCrops = new SparseArray<>();
+            suggestedCrops.put(unfoldedOne, unfoldedCropOne);
+            suggestedCrops.put(unfoldedTwo, unfoldedCropTwo);
+
+            int foldedOne = getFoldedOrientation(unfoldedOne);
+            int foldedTwo = getFoldedOrientation(unfoldedTwo);
+            Point foldedDisplayOne = mDisplaySizes.get(foldedOne);
+            Point foldedDisplayTwo = mDisplaySizes.get(foldedTwo);
+
+            for (boolean rtl : List.of(false, true)) {
+                assertThat(mWallpaperCropper.getCrop(
+                        foldedDisplayOne, bitmapSize, suggestedCrops, rtl))
+                        .isEqualTo(centerOf(unfoldedCropOne, foldedDisplayOne));
+
+                assertThat(mWallpaperCropper.getCrop(
+                        foldedDisplayTwo, bitmapSize, suggestedCrops, rtl))
+                        .isEqualTo(centerOf(unfoldedCropTwo, foldedDisplayTwo));
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop}, when asked for an unfolded crop with a suggested
+     * crop only for the relative folded orientation, creates the unfolded crop with the same center
+     * as the folded crop, by adding content on two sides to match the unfolded screen dimensions.
+     * <p>
+     * To simplify, in this test case all crops have the same size as the display (no zoom) and are
+     * at the center of the image. Also the image is large enough to add content.
+     */
+    @Test
+    public void testGetCrop_hasFoldedSuggestedCrop() {
+        for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
+            setUpWithDisplays(displaySizes);
+            Point bitmapSize = new Point(2000, 2000);
+            Rect bitmapRect = new Rect(0, 0, 2000, 2000);
+
+            Point smallestDisplay = displaySizes.stream().min(
+                    Comparator.comparingInt(a -> a.x * a.y)).orElseThrow();
+            int foldedOne = getOrientation(smallestDisplay);
+            int foldedTwo = getRotatedOrientation(foldedOne);
+            Point foldedDisplayOne = mDisplaySizes.get(foldedOne);
+            Point foldedDisplayTwo = mDisplaySizes.get(foldedTwo);
+            Rect foldedCropOne = centerOf(bitmapRect, foldedDisplayOne);
+            Rect foldedCropTwo = centerOf(bitmapRect, foldedDisplayTwo);
+            SparseArray<Rect> suggestedCrops = new SparseArray<>();
+            suggestedCrops.put(foldedOne, foldedCropOne);
+            suggestedCrops.put(foldedTwo, foldedCropTwo);
+
+            int unfoldedOne = getUnfoldedOrientation(foldedOne);
+            int unfoldedTwo = getUnfoldedOrientation(foldedTwo);
+            Point unfoldedDisplayOne = mDisplaySizes.get(unfoldedOne);
+            Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo);
+
+            for (boolean rtl : List.of(false, true)) {
+                assertThat(centerOf(mWallpaperCropper.getCrop(
+                        unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne))
+                        .isEqualTo(foldedCropOne);
+
+                assertThat(centerOf(mWallpaperCropper.getCrop(
+                        unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo))
+                        .isEqualTo(foldedCropTwo);
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop}, when asked for an folded crop with a suggested
+     * crop only for the rotated unfolded orientation, creates the folded crop from that crop by
+     * combining a rotate + fold operation. The folded crop should have less pixels than the
+     * unfolded crop due to the fold operation which removes content on both sides of the image.
+     * <p>
+     * To simplify, in this test case all crops have the same size as the display (no zoom)
+     * and are at the center of the image.
+     */
+    @Test
+    public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() {
+        for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
+            setUpWithDisplays(displaySizes);
+            Point bitmapSize = new Point(2000, 2000);
+            Rect bitmapRect = new Rect(0, 0, 2000, 2000);
+            Point largestDisplay = displaySizes.stream().max(
+                    Comparator.comparingInt(a -> a.x * a.y)).orElseThrow();
+            int unfoldedOne = getOrientation(largestDisplay);
+            int unfoldedTwo = getRotatedOrientation(unfoldedOne);
+            for (int unfolded: List.of(unfoldedOne, unfoldedTwo)) {
+                Rect unfoldedCrop = centerOf(bitmapRect, mDisplaySizes.get(unfolded));
+                int rotatedUnfolded = getRotatedOrientation(unfolded);
+                Rect rotatedUnfoldedCrop = centerOf(bitmapRect, mDisplaySizes.get(rotatedUnfolded));
+                SparseArray<Rect> suggestedCrops = new SparseArray<>();
+                suggestedCrops.put(unfolded, unfoldedCrop);
+                int rotatedFolded = getFoldedOrientation(rotatedUnfolded);
+                Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded);
+
+                for (boolean rtl : List.of(false, true)) {
+                    assertThat(mWallpaperCropper.getCrop(
+                            rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl))
+                            .isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay));
+                }
+            }
+        }
+    }
+
+    /**
+     * Test that {@link WallpaperCropper#getCrop}, when asked for an unfolded crop with a suggested
+     * crop only for the rotated folded orientation, creates the unfolded crop from that crop by
+     * combining a rotate + unfold operation. The unfolded crop should have more pixels than the
+     * folded crop due to the unfold operation which adds content on two sides of the image.
+     * <p>
+     * To simplify, in this test case all crops have the same size as the display (no zoom)
+     * and are centered inside the image. Also the image is large enough to add content.
+     */
+    @Test
+    public void testGetCrop_hasRotatedFoldedSuggestedCrop() {
+        for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
+            setUpWithDisplays(displaySizes);
+            Point bitmapSize = new Point(2000, 2000);
+            Rect bitmapRect = new Rect(0, 0, 2000, 2000);
+
+            Point smallestDisplay = displaySizes.stream().min(
+                    Comparator.comparingInt(a -> a.x * a.y)).orElseThrow();
+            int foldedOne = getOrientation(smallestDisplay);
+            int foldedTwo = getRotatedOrientation(foldedOne);
+            for (int folded: List.of(foldedOne, foldedTwo)) {
+                Rect foldedCrop = centerOf(bitmapRect, mDisplaySizes.get(folded));
+                SparseArray<Rect> suggestedCrops = new SparseArray<>();
+                suggestedCrops.put(folded, foldedCrop);
+                int rotatedFolded = getRotatedOrientation(folded);
+                int rotatedUnfolded = getUnfoldedOrientation(rotatedFolded);
+                Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded);
+                Rect rotatedFoldedCrop = centerOf(bitmapRect, rotatedFoldedDisplay);
+                Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded);
+
+                for (boolean rtl : List.of(false, true)) {
+                    Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop(
+                            rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl);
+                    assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay))
+                            .isEqualTo(rotatedFoldedCrop);
+                }
+            }
+        }
+    }
+
+    private static Rect centerOf(Rect container, Point point) {
+        checkSubset(container, point);
+        int diffWidth = container.width() - point.x;
+        int diffHeight = container.height() - point.y;
+        int startX = container.left + diffWidth / 2;
+        int startY = container.top + diffHeight / 2;
+        return new Rect(startX, startY, startX + point.x, startY + point.y);
+    }
+
+    private static Rect leftOf(Rect container, Point point) {
+        Rect result = centerOf(container, point);
+        result.offset(container.left - result.left, 0);
+        return result;
+    }
+
+    private static Rect rightOf(Rect container, Point point) {
+        checkSubset(container, point);
+        Rect result = centerOf(container, point);
+        result.offset(container.right - result.right, 0);
+        return result;
+    }
+
+    private static void checkSubset(Rect container, Point point) {
+        if (container.width() < point.x || container.height() < point.y) {
+            throw new IllegalArgumentException();
+        }
+    }
+}
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 5e5181b..0089d4c 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -109,7 +109,6 @@
     <uses-permission android:name="android.permission.UPDATE_LOCK_TASK_PACKAGES" />
     <uses-permission android:name="android.permission.ACCESS_CONTEXT_HUB" />
     <uses-permission android:name="android.permission.USE_BIOMETRIC_INTERNAL" />
-    <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" />
     <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
     <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
index 4db27d2..dc26e6e 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
@@ -17,9 +17,9 @@
 package com.android.server.accessibility;
 
 import static com.android.server.accessibility.AbstractAccessibilityServiceConnection.DISPLAY_TYPE_DEFAULT;
-import static com.android.server.accessibility.AccessibilityWindowManagerTest.DisplayIdMatcher.displayId;
-import static com.android.server.accessibility.AccessibilityWindowManagerTest.WindowChangesMatcher.a11yWindowChanges;
-import static com.android.server.accessibility.AccessibilityWindowManagerTest.WindowIdMatcher.a11yWindowId;
+import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.DisplayIdMatcher.displayId;
+import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowChangesMatcher.a11yWindowChanges;
+import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowIdMatcher.a11yWindowId;
 
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -27,11 +27,13 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -42,6 +44,7 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
+import android.graphics.Point;
 import android.graphics.Region;
 import android.os.IBinder;
 import android.os.LocaleList;
@@ -63,6 +66,7 @@
 
 import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection;
 import com.android.server.accessibility.test.MessageCapturingHandler;
+import com.android.server.wm.AccessibilityWindowsPopulator.AccessibilityWindow;
 import com.android.server.wm.WindowManagerInternal;
 import com.android.server.wm.WindowManagerInternal.WindowsForAccessibilityCallback;
 
@@ -117,9 +121,8 @@
 
     // List of window token, mapping from windowId -> window token.
     private final SparseArray<IWindow> mA11yWindowTokens = new SparseArray<>();
-    // List of window info lists, mapping from displayId -> window info lists.
-    private final SparseArray<ArrayList<WindowInfo>> mWindowInfos =
-            new SparseArray<>();
+    // List of window info lists, mapping from displayId -> a11y window lists.
+    private final SparseArray<ArrayList<AccessibilityWindow>> mWindows = new SparseArray<>();
     // List of callback, mapping from displayId -> callback.
     private final SparseArray<WindowsForAccessibilityCallback> mCallbackOfWindows =
             new SparseArray<>();
@@ -129,6 +132,13 @@
 
     private final MessageCapturingHandler mHandler = new MessageCapturingHandler(null);
 
+    // This maps displayId -> next region offset.
+    // Touchable region must have un-occluded area so that it's exposed to a11y services.
+    // This offset can be used as left and top of new region so that top-left of each region are
+    // kept visible.
+    // It's expected to be incremented by some amount everytime the value is used.
+    private final SparseArray<Integer> mNextRegionOffsets = new SparseArray<>();
+
     @Mock
     private WindowManagerInternal mMockWindowManagerInternal;
     @Mock
@@ -162,7 +172,7 @@
                 anyString(), anyInt(), anyInt(), anyInt())).thenReturn(PACKAGE_NAME);
 
         doAnswer((invocation) -> {
-            onWindowsForAccessibilityChanged(invocation.getArgument(0), false);
+            onAccessibilityWindowsChanged(invocation.getArgument(0), false);
             return null;
         }).when(mMockWindowManagerInternal).computeWindowsForAccessibility(anyInt());
 
@@ -176,7 +186,7 @@
         // as top focused display before each testing starts.
         startTrackingPerDisplay(Display.DEFAULT_DISPLAY);
 
-        // AccessibilityEventSender is invoked during onWindowsForAccessibilityChanged.
+        // AccessibilityEventSender is invoked during onAccessibilityWindowsChanged.
         // Resets it for mockito verify of further test case.
         Mockito.reset(mMockA11yEventSender);
 
@@ -240,19 +250,18 @@
     @Test
     public void onWindowsChanged_duringTouchInteractAndFocusChange_shouldChangeActiveWindow() {
         final int activeWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID);
-        WindowInfo focusedWindowInfo =
-                mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX);
+        final WindowInfo focusedWindowInfo =
+                mWindows.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).getWindowInfo();
         assertEquals(activeWindowId, mA11yWindowManager.findWindowIdLocked(
                 USER_SYSTEM_ID, focusedWindowInfo.token));
 
         focusedWindowInfo.focused = false;
-        focusedWindowInfo =
-                mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX + 1);
-        focusedWindowInfo.focused = true;
+        mWindows.get(Display.DEFAULT_DISPLAY).get(
+                DEFAULT_FOCUSED_INDEX + 1).getWindowInfo().focused = true;
 
         mA11yWindowManager.onTouchInteractionStart();
         setTopFocusedWindowAndDisplay(Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX + 1);
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
 
         assertNotEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID));
     }
@@ -276,7 +285,7 @@
         changeFocusedWindowOnDisplayPerDisplayFocusConfig(SECONDARY_DISPLAY_ID,
                 DEFAULT_FOCUSED_INDEX + 1, Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX);
 
-        onWindowsForAccessibilityChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES);
         // The active window should not be changed.
         assertEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID));
         // The top focused window should not be changed.
@@ -304,8 +313,8 @@
         changeFocusedWindowOnDisplayPerDisplayFocusConfig(SECONDARY_DISPLAY_ID,
                 DEFAULT_FOCUSED_INDEX, Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX);
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
-        onWindowsForAccessibilityChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES);
         // The active window should be changed.
         assertNotEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID));
         // The top focused window should be changed.
@@ -315,53 +324,43 @@
 
     @Test
     public void onWindowsChanged_shouldReportCorrectLayer() {
-        // AccessibilityWindowManager#onWindowsForAccessibilityChanged already invoked in setup.
+        // AccessibilityWindowManager#onAccessibilityWindowsChanged already invoked in setup.
         List<AccessibilityWindowInfo> a11yWindows =
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
         for (int i = 0; i < a11yWindows.size(); i++) {
             final AccessibilityWindowInfo a11yWindow = a11yWindows.get(i);
-            final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(i);
-            assertThat(mWindowInfos.get(Display.DEFAULT_DISPLAY).size() - windowInfo.layer - 1,
+            assertThat(mWindows.get(Display.DEFAULT_DISPLAY).size() - i - 1,
                     is(a11yWindow.getLayer()));
         }
     }
 
     @Test
     public void onWindowsChanged_shouldReportCorrectOrder() {
-        // AccessibilityWindowManager#onWindowsForAccessibilityChanged already invoked in setup.
+        // AccessibilityWindowManager#onAccessibilityWindowsChanged already invoked in setup.
         List<AccessibilityWindowInfo> a11yWindows =
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
         for (int i = 0; i < a11yWindows.size(); i++) {
             final AccessibilityWindowInfo a11yWindow = a11yWindows.get(i);
             final IBinder windowToken = mA11yWindowManager
                     .getWindowTokenForUserAndWindowIdLocked(USER_SYSTEM_ID, a11yWindow.getId());
-            final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(i);
+            final WindowInfo windowInfo = mWindows.get(Display.DEFAULT_DISPLAY)
+                    .get(i).getWindowInfo();
             assertThat(windowToken, is(windowInfo.token));
         }
     }
 
     @Test
     public void onWindowsChangedAndForceSend_shouldUpdateWindows() {
-        final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0);
-        final int correctLayer =
-                mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer();
-        windowInfo.layer += 1;
+        assertNotEquals("new title",
+                toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)
+                        .get(0).getTitle()));
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
-        assertNotEquals(correctLayer,
-                mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer());
-    }
+        mWindows.get(Display.DEFAULT_DISPLAY).get(0).getWindowInfo().title = "new title";
 
-    @Test
-    public void onWindowsChangedNoForceSend_layerChanged_shouldNotUpdateWindows() {
-        final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0);
-        final int correctLayer =
-                mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer();
-        windowInfo.layer += 1;
-
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
-        assertEquals(correctLayer,
-                mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer());
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
+        assertEquals("new title",
+                toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)
+                        .get(0).getTitle()));
     }
 
     @Test
@@ -371,14 +370,10 @@
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0);
         final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY,
                 true, USER_SYSTEM_ID);
-        final WindowInfo windowInfo = WindowInfo.obtain();
-        windowInfo.type = AccessibilityWindowInfo.TYPE_APPLICATION;
-        windowInfo.token = token.asBinder();
-        windowInfo.layer = 0;
-        windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
-        mWindowInfos.get(Display.DEFAULT_DISPLAY).set(0, windowInfo);
+        mWindows.get(Display.DEFAULT_DISPLAY).set(0,
+                createMockAccessibilityWindow(token, Display.DEFAULT_DISPLAY));
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
         assertNotEquals(oldWindow,
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0));
     }
@@ -386,12 +381,12 @@
     @Test
     public void onWindowsChangedNoForceSend_focusChanged_shouldUpdateWindows() {
         final WindowInfo focusedWindowInfo =
-                mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX);
-        final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0);
+                mWindows.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).getWindowInfo();
+        final WindowInfo windowInfo = mWindows.get(Display.DEFAULT_DISPLAY).get(0).getWindowInfo();
         focusedWindowInfo.focused = false;
         windowInfo.focused = true;
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
         assertTrue(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0)
                 .isFocused());
     }
@@ -500,15 +495,18 @@
     @Test
     public void computePartialInteractiveRegionForWindow_wholeVisible_returnWholeRegion() {
         // Updates top 2 z-order WindowInfo are whole visible.
-        WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0);
-        windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
-        windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(1);
-        windowInfo.regionInScreen.set(0, SCREEN_HEIGHT / 2,
-                SCREEN_WIDTH, SCREEN_HEIGHT);
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0);
+        setRegionForMockAccessibilityWindow(firstWindow,
+                new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2));
+        final AccessibilityWindow secondWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(1);
+        setRegionForMockAccessibilityWindow(secondWindow,
+                new Region(0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT));
+
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
 
         final List<AccessibilityWindowInfo> a11yWindows =
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
+        assertThat(a11yWindows, hasSize(2));
         final Region outBounds = new Region();
         int windowId = a11yWindows.get(0).getId();
 
@@ -526,12 +524,17 @@
     @Test
     public void computePartialInteractiveRegionForWindow_halfVisible_returnHalfRegion() {
         // Updates z-order #1 WindowInfo is half visible.
-        WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0);
-        windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
+        final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0);
+        setRegionForMockAccessibilityWindow(firstWindow,
+                new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2));
+        final AccessibilityWindow secondWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(1);
+        setRegionForMockAccessibilityWindow(secondWindow,
+                new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT));
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
         final List<AccessibilityWindowInfo> a11yWindows =
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
+        assertThat(a11yWindows, hasSize(2));
         final Region outBounds = new Region();
         int windowId = a11yWindows.get(1).getId();
 
@@ -542,9 +545,17 @@
 
     @Test
     public void computePartialInteractiveRegionForWindow_notVisible_returnEmptyRegion() {
-        // Since z-order #0 WindowInfo is full screen, z-order #1 WindowInfo should be invisible.
+        // z-order #0 WindowInfo is full screen, z-order #1 WindowInfo should be invisible.
+        final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0);
+        setRegionForMockAccessibilityWindow(firstWindow,
+                new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT));
+
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+
         final List<AccessibilityWindowInfo> a11yWindows =
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
+        // Note that the second window is also exposed even if region is empty because it's focused.
+        assertThat(a11yWindows, hasSize(2));
         final Region outBounds = new Region();
         int windowId = a11yWindows.get(1).getId();
 
@@ -555,16 +566,21 @@
     @Test
     public void computePartialInteractiveRegionForWindow_partialVisible_returnVisibleRegion() {
         // Updates z-order #0 WindowInfo to have two interact-able areas.
-        Region region = new Region(0, 0, SCREEN_WIDTH, 200);
+        final Region region = new Region(0, 0, SCREEN_WIDTH, 200);
         region.op(0, SCREEN_HEIGHT - 200, SCREEN_WIDTH, SCREEN_HEIGHT, Region.Op.UNION);
-        WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0);
-        windowInfo.regionInScreen.set(region);
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0);
+        setRegionForMockAccessibilityWindow(firstWindow, region);
+        final AccessibilityWindow secondWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(1);
+        setRegionForMockAccessibilityWindow(secondWindow,
+                new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT));
+
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
 
         final List<AccessibilityWindowInfo> a11yWindows =
                 mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
+        assertThat(a11yWindows, hasSize(2));
         final Region outBounds = new Region();
-        int windowId = a11yWindows.get(1).getId();
+        final int windowId = a11yWindows.get(1).getId();
 
         mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds);
         assertFalse(outBounds.getBounds().isEmpty());
@@ -575,7 +591,8 @@
     @Test
     public void updateActiveAndA11yFocusedWindow_windowStateChangedEvent_noTracking_shouldUpdate() {
         final IBinder eventWindowToken =
-                mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX + 1).token;
+                mWindows.get(Display.DEFAULT_DISPLAY)
+                        .get(DEFAULT_FOCUSED_INDEX + 1).getWindowInfo().token;
         final int eventWindowId = mA11yWindowManager.findWindowIdLocked(
                 USER_SYSTEM_ID, eventWindowToken);
         when(mMockWindowManagerInternal.getFocusedWindowTokenFromWindowStates())
@@ -766,7 +783,8 @@
     public void onTouchInteractionEnd_noServiceInteractiveWindow_shouldClearA11yFocus()
             throws RemoteException {
         final IBinder defaultFocusWinToken =
-                mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).token;
+                mWindows.get(Display.DEFAULT_DISPLAY).get(
+                        DEFAULT_FOCUSED_INDEX).getWindowInfo().token;
         final int defaultFocusWindowId = mA11yWindowManager.findWindowIdLocked(
                 USER_SYSTEM_ID, defaultFocusWinToken);
         when(mMockWindowManagerInternal.getFocusedWindowTokenFromWindowStates())
@@ -811,8 +829,8 @@
     @Test
     public void getPictureInPictureWindow_shouldNotNull() {
         assertNull(mA11yWindowManager.getPictureInPictureWindowLocked());
-        mWindowInfos.get(Display.DEFAULT_DISPLAY).get(1).inPictureInPicture = true;
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        mWindows.get(Display.DEFAULT_DISPLAY).get(1).getWindowInfo().inPictureInPicture = true;
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
 
         assertNotNull(mA11yWindowManager.getPictureInPictureWindowLocked());
     }
@@ -826,8 +844,9 @@
         final IAccessibilityInteractionConnection mockRemoteConnection =
                 mA11yWindowManager.getConnectionLocked(
                         USER_SYSTEM_ID, outsideWindowId).getRemote();
-        mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0).hasFlagWatchOutsideTouch = true;
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+        mWindows.get(Display.DEFAULT_DISPLAY).get(0).getWindowInfo().hasFlagWatchOutsideTouch =
+                true;
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
 
         mA11yWindowManager.notifyOutsideTouch(USER_SYSTEM_ID, targetWindowId);
         verify(mockRemoteConnection).notifyOutsideTouch();
@@ -945,18 +964,14 @@
 
     @Test
     public void sendAccessibilityEventOnWindowRemoval() {
-        final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY);
+        final ArrayList<AccessibilityWindow> windows = mWindows.get(Display.DEFAULT_DISPLAY);
 
         // Removing index 0 because it's not focused, and avoids unnecessary layer change.
         final int windowId =
                 getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0);
-        infos.remove(0);
-        for (WindowInfo info : infos) {
-            // Adjust layer number because it should start from 0.
-            info.layer--;
-        }
+        windows.remove(0);
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
 
         final ArgumentCaptor<AccessibilityEvent> captor =
                 ArgumentCaptor.forClass(AccessibilityEvent.class);
@@ -970,21 +985,15 @@
 
     @Test
     public void sendAccessibilityEventOnWindowAddition() throws RemoteException {
-        final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY);
-
-        for (WindowInfo info : infos) {
-            // Adjust layer number because new window will have 0 so that layer number in
-            // A11yWindowInfo in window won't be changed.
-            info.layer++;
-        }
+        final ArrayList<AccessibilityWindow> windows = mWindows.get(Display.DEFAULT_DISPLAY);
 
         final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY,
                 false, USER_SYSTEM_ID);
-        addWindowInfo(infos, token, 0);
-        final int windowId =
-                getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, infos.size() - 1);
+        // Adding window to the front so that other windows' layer won't change.
+        windows.add(0, createMockAccessibilityWindow(token, Display.DEFAULT_DISPLAY));
+        final int windowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0);
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
 
         final ArgumentCaptor<AccessibilityEvent> captor =
                 ArgumentCaptor.forClass(AccessibilityEvent.class);
@@ -998,11 +1007,11 @@
 
     @Test
     public void sendAccessibilityEventOnWindowChange() {
-        final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY);
-        infos.get(0).title = "new title";
+        final ArrayList<AccessibilityWindow> windows = mWindows.get(Display.DEFAULT_DISPLAY);
+        windows.get(0).getWindowInfo().title = "new title";
         final int windowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0);
 
-        onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND);
 
         final ArgumentCaptor<AccessibilityEvent> captor =
                 ArgumentCaptor.forClass(AccessibilityEvent.class);
@@ -1020,42 +1029,41 @@
     }
 
     private void startTrackingPerDisplay(int displayId) throws RemoteException {
-        ArrayList<WindowInfo> windowInfosForDisplay = new ArrayList<>();
+        ArrayList<AccessibilityWindow> windowsForDisplay = new ArrayList<>();
         // Adds RemoteAccessibilityConnection into AccessibilityWindowManager, and copy
         // mock window token into mA11yWindowTokens. Also, preparing WindowInfo mWindowInfos
         // for the test.
-        int layer = 0;
         for (int i = 0; i < NUM_GLOBAL_WINDOWS; i++) {
             final IWindow token = addAccessibilityInteractionConnection(displayId,
                     true, USER_SYSTEM_ID);
-            addWindowInfo(windowInfosForDisplay, token, layer++);
+            windowsForDisplay.add(createMockAccessibilityWindow(token, displayId));
 
         }
         for (int i = 0; i < NUM_APP_WINDOWS; i++) {
             final IWindow token = addAccessibilityInteractionConnection(displayId,
                     false, USER_SYSTEM_ID);
-            addWindowInfo(windowInfosForDisplay, token, layer++);
+            windowsForDisplay.add(createMockAccessibilityWindow(token, displayId));
         }
         // Sets up current focused window of display.
         // Each display has its own current focused window if config_perDisplayFocusEnabled is true.
         // Otherwise only default display needs to current focused window.
         if (mSupportPerDisplayFocus || displayId == Display.DEFAULT_DISPLAY) {
-            windowInfosForDisplay.get(DEFAULT_FOCUSED_INDEX).focused = true;
+            windowsForDisplay.get(DEFAULT_FOCUSED_INDEX).getWindowInfo().focused = true;
         }
         // Turns on windows tracking, and update window info.
         mA11yWindowManager.startTrackingWindows(displayId, false);
         // Puts window lists into array.
-        mWindowInfos.put(displayId, windowInfosForDisplay);
+        mWindows.put(displayId, windowsForDisplay);
         // Sets the default display is the top focused display and
         // its current focused window is the top focused window.
         if (displayId == Display.DEFAULT_DISPLAY) {
             setTopFocusedWindowAndDisplay(displayId, DEFAULT_FOCUSED_INDEX);
         }
         // Invokes callback for sending window lists to A11y framework.
-        onWindowsForAccessibilityChanged(displayId, FORCE_SEND);
+        onAccessibilityWindowsChanged(displayId, FORCE_SEND);
 
         assertEquals(mA11yWindowManager.getWindowListLocked(displayId).size(),
-                windowInfosForDisplay.size());
+                windowsForDisplay.size());
     }
 
     private WindowsForAccessibilityCallback getWindowsForAccessibilityCallbacks(int displayId) {
@@ -1109,36 +1117,28 @@
         return windowId;
     }
 
-    private void addWindowInfo(ArrayList<WindowInfo> windowInfos, IWindow windowToken, int layer) {
-        final WindowInfo windowInfo = WindowInfo.obtain();
-        windowInfo.type = AccessibilityWindowInfo.TYPE_APPLICATION;
-        windowInfo.token = windowToken.asBinder();
-        windowInfo.layer = layer;
-        windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
-        windowInfos.add(windowInfo);
-    }
-
     private int getWindowIdFromWindowInfosForDisplay(int displayId, int index) {
-        final IBinder windowToken = mWindowInfos.get(displayId).get(index).token;
+        final IBinder windowToken = mWindows.get(displayId).get(index).getWindowInfo().token;
         return mA11yWindowManager.findWindowIdLocked(
                 USER_SYSTEM_ID, windowToken);
     }
 
     private void setTopFocusedWindowAndDisplay(int displayId, int index) {
         // Sets the top focus window.
-        mTopFocusedWindowToken = mWindowInfos.get(displayId).get(index).token;
+        mTopFocusedWindowToken = mWindows.get(displayId).get(index).getWindowInfo().token;
         // Sets the top focused display.
         mTopFocusedDisplayId = displayId;
     }
 
-    private void onWindowsForAccessibilityChanged(int displayId, boolean forceSend) {
+    private void onAccessibilityWindowsChanged(int displayId, boolean forceSend) {
         WindowsForAccessibilityCallback callbacks = mCallbackOfWindows.get(displayId);
         if (callbacks == null) {
             callbacks = getWindowsForAccessibilityCallbacks(displayId);
             mCallbackOfWindows.put(displayId, callbacks);
         }
-        callbacks.onWindowsForAccessibilityChanged(forceSend, mTopFocusedDisplayId,
-                mTopFocusedWindowToken, mWindowInfos.get(displayId));
+        callbacks.onAccessibilityWindowsChanged(forceSend, mTopFocusedDisplayId,
+                mTopFocusedWindowToken, new Point(SCREEN_WIDTH, SCREEN_HEIGHT),
+                mWindows.get(displayId));
     }
 
     private void changeFocusedWindowOnDisplayPerDisplayFocusConfig(
@@ -1147,23 +1147,23 @@
         if (mSupportPerDisplayFocus) {
             // Gets the old focused window of display which wants to change focused window.
             WindowInfo focusedWindowInfo =
-                    mWindowInfos.get(changeFocusedDisplayId).get(oldFocusedWindowIndex);
+                    mWindows.get(changeFocusedDisplayId).get(oldFocusedWindowIndex).getWindowInfo();
             // Resets the focus of old focused window.
             focusedWindowInfo.focused = false;
             // Gets the new window of display which wants to change focused window.
             focusedWindowInfo =
-                    mWindowInfos.get(changeFocusedDisplayId).get(newFocusedWindowIndex);
+                    mWindows.get(changeFocusedDisplayId).get(newFocusedWindowIndex).getWindowInfo();
             // Sets the focus of new focused window.
             focusedWindowInfo.focused = true;
         } else {
             // Gets the window of display which wants to change focused window.
             WindowInfo focusedWindowInfo =
-                    mWindowInfos.get(changeFocusedDisplayId).get(newFocusedWindowIndex);
+                    mWindows.get(changeFocusedDisplayId).get(newFocusedWindowIndex).getWindowInfo();
             // Sets the focus of new focused window.
             focusedWindowInfo.focused = true;
             // Gets the old focused window of old top focused display.
             focusedWindowInfo =
-                    mWindowInfos.get(oldTopFocusedDisplayId).get(oldFocusedWindowIndex);
+                    mWindows.get(oldTopFocusedDisplayId).get(oldFocusedWindowIndex).getWindowInfo();
             // Resets the focus of old focused window.
             focusedWindowInfo.focused = false;
             // Changes the top focused display and window.
@@ -1171,6 +1171,42 @@
         }
     }
 
+    private AccessibilityWindow createMockAccessibilityWindow(IWindow windowToken, int displayId) {
+        final WindowInfo windowInfo = WindowInfo.obtain();
+        // TODO(b/325341171): add tests with various kinds of windows such as
+        //  changing window types, touchable or not, trusted or not, etc.
+        windowInfo.type = WindowManager.LayoutParams.TYPE_APPLICATION;
+        windowInfo.token = windowToken.asBinder();
+
+        final AccessibilityWindow window = Mockito.mock(AccessibilityWindow.class);
+        when(window.getWindowInfo()).thenReturn(windowInfo);
+        when(window.ignoreRecentsAnimationForAccessibility()).thenReturn(false);
+        when(window.isFocused()).thenAnswer(invocation -> windowInfo.focused);
+        when(window.isTouchable()).thenReturn(true);
+        when(window.getType()).thenReturn(windowInfo.type);
+
+        setRegionForMockAccessibilityWindow(window, nextToucableRegion(displayId));
+        return window;
+    }
+
+    private void setRegionForMockAccessibilityWindow(AccessibilityWindow window, Region region) {
+        doAnswer(invocation -> {
+            ((Region) invocation.getArgument(0)).set(region);
+            return null;
+        }).when(window).getTouchableRegionInScreen(any(Region.class));
+        doAnswer(invocation -> {
+            ((Region) invocation.getArgument(0)).set(region);
+            return null;
+        }).when(window).getTouchableRegionInWindow(any(Region.class));
+    }
+
+    private Region nextToucableRegion(int displayId) {
+        final int topLeft = mNextRegionOffsets.get(displayId, 0);
+        final int bottomRight = topLeft + 100;
+        mNextRegionOffsets.put(displayId, topLeft + 10);
+        return new Region(topLeft, topLeft, bottomRight, bottomRight);
+    }
+
     @Nullable
     private static String toString(@Nullable CharSequence cs) {
         return cs == null ? null : cs.toString();
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 8c0d44c..7fbd521 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -189,6 +189,8 @@
     FullScreenMagnificationVibrationHelper mMockFullScreenMagnificationVibrationHelper;
     @Mock
     FullScreenMagnificationGestureHandler.MagnificationLogger mMockMagnificationLogger;
+    @Mock
+    OneFingerPanningSettingsProvider mMockOneFingerPanningSettingsProvider;
 
     @Rule
     public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
@@ -266,6 +268,7 @@
         mMgh.onDestroy();
         mFullScreenMagnificationController.unregister(DISPLAY_0);
         verify(mWindowMagnificationPromptController).onDestroy();
+        verify(mMockOneFingerPanningSettingsProvider).unregister();
         Settings.Secure.putFloatForUser(mContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
                 mOriginalMagnificationPersistedScale,
@@ -288,11 +291,10 @@
                         DISPLAY_0,
                         mMockFullScreenMagnificationVibrationHelper,
                         mMockMagnificationLogger,
-                        ViewConfiguration.get(mContext));
+                        ViewConfiguration.get(mContext),
+                        mMockOneFingerPanningSettingsProvider);
         if (isWatch()) {
-            h.setSinglePanningEnabled(true);
-        } else {
-            h.setSinglePanningEnabled(false);
+            enableOneFingerPanning(true);
         }
         mHandler = new TestHandler(h.mDetectingState, mClock) {
             @Override
@@ -607,8 +609,8 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
     public void testTwoFingerTap_StateIsActivated_shouldInDelegating() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
-        mMgh.setSinglePanningEnabled(false);
+        assumeTrue(isWatch());
+        enableOneFingerPanning(false);
         goFromStateIdleTo(STATE_ACTIVATED);
         allowEventDelegation();
 
@@ -623,8 +625,8 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
     public void testTwoFingerTap_StateIsIdle_shouldInDelegating() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
-        mMgh.setSinglePanningEnabled(false);
+        assumeTrue(isWatch());
+        enableOneFingerPanning(false);
         goFromStateIdleTo(STATE_IDLE);
         allowEventDelegation();
 
@@ -830,7 +832,7 @@
 
     @Test
     public void testActionUpNotAtEdge_singlePanningState_detectingState() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
 
         send(upEvent());
@@ -841,8 +843,8 @@
 
     @Test
     public void testScroll_SinglePanningDisabled_delegatingState() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
-        mMgh.setSinglePanningEnabled(false);
+        assumeTrue(isWatch());
+        enableOneFingerPanning(false);
 
         goFromStateIdleTo(STATE_ACTIVATED);
         allowEventDelegation();
@@ -854,7 +856,7 @@
     @Test
     @FlakyTest
     public void testScroll_singleHorizontalPanningAndAtEdge_leftEdgeOverscroll() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
         float centerY =
                 (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
@@ -878,7 +880,7 @@
     @Test
     @FlakyTest
     public void testScroll_singleHorizontalPanningAndAtEdge_rightEdgeOverscroll() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
         float centerY =
                 (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
@@ -902,7 +904,7 @@
     @Test
     @FlakyTest
     public void testScroll_singleVerticalPanningAndAtEdge_verticalOverscroll() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
         float centerX =
                 (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f;
@@ -924,7 +926,7 @@
 
     @Test
     public void testScroll_singlePanningAndAtEdge_noOverscroll() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
         float centerY =
                 (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
@@ -946,7 +948,7 @@
 
     @Test
     public void testScroll_singleHorizontalPanningAndAtEdge_vibrate() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
         mFullScreenMagnificationController.setCenter(
                 DISPLAY_0,
@@ -970,7 +972,7 @@
 
     @Test
     public void testScroll_singleVerticalPanningAndAtEdge_doNotVibrate() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
         mFullScreenMagnificationController.setCenter(
                 DISPLAY_0,
@@ -993,8 +995,9 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
     public void singleFinger_testScrollAfterMagnified_startsFling() {
-        assumeTrue(mMgh.mIsSinglePanningEnabled);
+        assumeTrue(isWatch());
         goFromStateIdleTo(STATE_ACTIVATED);
 
         swipeAndHold();
@@ -1274,6 +1277,10 @@
         mFullScreenMagnificationController.reset(DISPLAY_0, /* animate= */ false);
     }
 
+    private void enableOneFingerPanning(boolean enable) {
+        when(mMockOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()).thenReturn(enable);
+    }
+
     private void assertActionsInOrder(List<MotionEvent> actualEvents,
             List<Integer> expectedActions) {
         assertTrue(actualEvents.size() == expectedActions.size());
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OneFingerPanningSettingsProviderTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OneFingerPanningSettingsProviderTest.java
new file mode 100644
index 0000000..ac46ef9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OneFingerPanningSettingsProviderTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.server.accessibility.magnification;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.accessibility.magnification.OneFingerPanningSettingsProvider.State;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class OneFingerPanningSettingsProviderTest {
+
+    @Rule
+    public final TestableContext mContext = new TestableContext(getInstrumentation().getContext());
+
+    private boolean mDefaultValue;
+    private boolean mOriginalIsOneFingerPanningEnabled;
+
+    private OneFingerPanningSettingsProvider mProvider;
+
+    @Before
+    public void setup() {
+        mDefaultValue = OneFingerPanningSettingsProvider.isOneFingerPanningEnabledDefault(mContext);
+        mOriginalIsOneFingerPanningEnabled = isSecureSettingsEnabled();
+    }
+
+    @After
+    public void tearDown() {
+        enableSecureSettings(mOriginalIsOneFingerPanningEnabled);
+        if (mProvider != null) {
+            mProvider.unregister();
+        }
+    }
+
+    @Test
+    public void isOneFingerPanningEnabled_flagDisabled_matchesDefault() {
+        mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ false);
+
+        assertThat(mProvider.isOneFingerPanningEnabled()).isEqualTo(mDefaultValue);
+    }
+
+    @Test
+    public void isOneFingerPanningEnabled_flagEnabledSettingEnabled_true() {
+        enableSecureSettings(true);
+        mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true);
+
+        assertTrue(mProvider.isOneFingerPanningEnabled());
+    }
+
+    @Test
+    public void isOneFingerPanningEnabled_flagEnabledSettingDisabled_false() {
+        enableSecureSettings(false);
+        mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true);
+
+        assertFalse(mProvider.isOneFingerPanningEnabled());
+    }
+
+    @Test
+    public void isOneFingerPanningEnabled_flagEnabledSettingsFalse_false() {
+        mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true);
+
+        // Simulate observer triggered.
+        enableSecureSettings(false);
+        mProvider.mObserver.onChange(/* selfChange= */ false);
+
+        assertFalse(mProvider.isOneFingerPanningEnabled());
+    }
+
+    @Test
+    public void isOneFingerPanningEnabled_flagEnabledSettingsTrue_true() {
+        mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true);
+
+        // Simulate observer triggered.
+        enableSecureSettings(true);
+        mProvider.mObserver.onChange(/* selfChange= */ false);
+
+        assertTrue(mProvider.isOneFingerPanningEnabled());
+    }
+
+    @Test
+    public void isOneFingerPanningEnabled_flagDisabledSettingsChanges_valueUnchanged() {
+        mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ false);
+        var previousValue = mProvider.isOneFingerPanningEnabled();
+
+        enableSecureSettings(!previousValue);
+
+        assertThat(mProvider.isOneFingerPanningEnabled()).isEqualTo(previousValue);
+        assertThat(mProvider.isOneFingerPanningEnabled()).isEqualTo(mDefaultValue);
+    }
+
+    @Test
+    public void unregister_featureEnabled_contentResolverNull() {
+        var provider = new OneFingerPanningSettingsProvider(
+                mContext, /* featureFlagEnabled */ true);
+
+        provider.unregister();
+
+        assertThat(provider.mContentResolver).isNull();
+    }
+
+    @Test
+    public void unregister_featureDisabled_noError() {
+        var provider = new OneFingerPanningSettingsProvider(
+                mContext, /* featureFlagEnabled */ false);
+
+        provider.unregister();
+    }
+
+    private void enableSecureSettings(boolean enable) {
+        Settings.Secure.putIntForUser(
+                mContext.getContentResolver(),
+                OneFingerPanningSettingsProvider.KEY,
+                enable ? State.ON : State.OFF,
+                mContext.getUserId());
+    }
+
+    private boolean isSecureSettingsEnabled() {
+        return State.ON == Settings.Secure.getIntForUser(
+                mContext.getContentResolver(),
+                OneFingerPanningSettingsProvider.KEY,
+                mDefaultValue ? State.ON : State.OFF,
+                mContext.getUserId());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index cea10ea..ea1a68a 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -825,7 +825,8 @@
         mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
         assertProfileLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* expectLocking= */ true);
         verifyUserUnassignedFromDisplay(TEST_USER_ID1);
@@ -842,7 +843,8 @@
         mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
         assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
                 /* keyEvictedCallback */ null, /* expectLocking= */ false);
@@ -852,19 +854,28 @@
     public void testStopPrivateProfileWithDelayedLocking_flagDisabled() throws Exception {
         mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
-        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         mSetFlagsRule.disableFlags(
                 android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
         setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
         assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
                 /* keyEvictedCallback */ null, /* expectLocking= */ true);
 
-        mSetFlagsRule.disableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.disableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         mSetFlagsRule.enableFlags(
                 android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
         setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_PRIVATE);
         assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ true,
                 /* keyEvictedCallback */ null, /* expectLocking= */ true);
+
+        mSetFlagsRule.disableFlags(android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
+        setUpAndStartProfileInBackground(TEST_USER_ID3, UserManager.USER_TYPE_PROFILE_PRIVATE);
+        assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ true,
+                /* keyEvictedCallback */ null, /* expectLocking= */ true);
     }
 
     /** Delayed-locking users (as opposed to devices) have no limits on how many can be unlocked. */
@@ -874,7 +885,8 @@
         mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
                 /* maxRunningUsers= */ 1, /* delayUserDataLocking= */ false);
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
         setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_MANAGED);
         assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
@@ -890,7 +902,8 @@
         mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
         mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
-                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
+                android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_MANAGED);
         assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
                 /* keyEvictedCallback */ null, /* expectLocking= */ true);
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/ApiCounterTest.kt b/services/tests/servicestests/src/com/android/server/appwidget/ApiCounterTest.kt
new file mode 100644
index 0000000..79766f8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appwidget/ApiCounterTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.appwidget
+
+import android.content.ComponentName
+import com.android.server.appwidget.AppWidgetServiceImpl.ApiCounter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ApiCounterTest {
+    private companion object {
+        const val RESET_INTERVAL_MS = 10L
+        const val MAX_CALLS_PER_INTERVAL = 2
+    }
+
+    private var currentTime = 0L
+
+    private val id =
+        AppWidgetServiceImpl.ProviderId(
+            /* uid= */ 123,
+            ComponentName("com.android.server.appwidget", "FakeProviderClass")
+        )
+    private val counter = ApiCounter(RESET_INTERVAL_MS, MAX_CALLS_PER_INTERVAL) { currentTime }
+
+    @Test
+    fun tryApiCall() {
+        for (i in 0 until MAX_CALLS_PER_INTERVAL) {
+            assertThat(counter.tryApiCall(id)).isTrue()
+        }
+        assertThat(counter.tryApiCall(id)).isFalse()
+        currentTime = 5L
+        assertThat(counter.tryApiCall(id)).isFalse()
+        currentTime = 11L
+        assertThat(counter.tryApiCall(id)).isTrue()
+    }
+
+    @Test
+    fun remove() {
+        for (i in 0 until MAX_CALLS_PER_INTERVAL) {
+            assertThat(counter.tryApiCall(id)).isTrue()
+        }
+        assertThat(counter.tryApiCall(id)).isFalse()
+        // remove should cause the call count to be 0 on the next tryApiCall
+        counter.remove(id)
+        assertThat(counter.tryApiCall(id)).isTrue()
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index bb00634..fa1fd90 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -344,6 +344,21 @@
         verifyNoMoreInteractions(mSensorManager);
     }
 
+    @Test
+    public void testAwaitLuxWhenNoLightSensor() {
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(null);
+        mProbe = new ALSProbe(mSensorManager, new Handler(mLooper.getLooper()), TIMEOUT_MS - 1);
+
+        AtomicInteger lux = new AtomicInteger(-5);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        // Verify that no light sensor will be registered.
+        verify(mSensorManager, times(0)).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        assertThat(lux.get()).isEqualTo(-1);
+    }
+
     private void moveTimeBy(long millis) {
         mLooper.moveTimeForward(millis);
         mLooper.processAllMessages();
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
index c8a5583d..3aaac2e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.biometrics.sensors.face;
 
-import static android.Manifest.permission.USE_BACKGROUND_FACE_AUTHENTICATION;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
 import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN;
@@ -235,26 +234,6 @@
     }
 
     @Test
-    public void testAuthenticateInBackground() throws Exception {
-        FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
-                .build();
-        initService();
-        mFaceService.mServiceWrapper.registerAuthenticators(List.of());
-        waitForRegistration();
-
-        mContext.getTestablePermissions().setPermission(
-                USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_DENIED);
-        mContext.getTestablePermissions().setPermission(
-                USE_BACKGROUND_FACE_AUTHENTICATION, PackageManager.PERMISSION_GRANTED);
-
-        final long operationId = 5;
-        mFaceService.mServiceWrapper.authenticateInBackground(mToken, operationId,
-                mFaceServiceReceiver, faceAuthenticateOptions);
-
-        assertThat(faceAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT);
-    }
-
-    @Test
     public void testOptionsForDetect() throws Exception {
         FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
                 .setOpPackageName(ComponentName.unflattenFromString(OP_PACKAGE_NAME)
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
index 9ad2652..6731403 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java
@@ -235,7 +235,7 @@
     }
 
     @Test
-    public void adjustOnlyAvbEnabled_audioDeviceVolumeChanged_doesNotSendSetAudioVolumeLevel() {
+    public void adjustOnlyAvbEnabled_audioDeviceVolumeChanged_requestsAndUpdatesAudioStatus() {
         enableAdjustOnlyAbsoluteVolumeBehavior();
 
         mNativeWrapper.clearResultMessages();
@@ -250,7 +250,22 @@
         );
         mTestLooper.dispatchAll();
 
-        assertThat(mNativeWrapper.getResultMessages()).isEmpty();
+        // We can't sent <Set Audio Volume Level> when using adjust-only AVB.
+        // Instead, we send <Give Audio Status>, to get the System Audio device's volume level.
+        // This ensures that we end up with a correct audio status in AudioService, even if it
+        // set it incorrectly because it assumed that we could send <Set Audio Volume Level>
+        assertThat(mNativeWrapper.getResultMessages().size()).isEqualTo(1);
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress())
+        );
+
+        // When we receive <Report Audio Status>, we notify AudioService of the volume level.
+        receiveReportAudioStatus(50,
+                true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(50 * STREAM_MUSIC_MAX_VOLUME / AudioStatus.MAX_VOLUME),
+                anyInt());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index 0aa72d0..98e119c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -184,13 +184,13 @@
 
     @Test
     public void isValid_setMenuLanguage() {
-        assertMessageValidity("4F:32:53:50:41").isEqualTo(OK);
+        assertMessageValidity("0F:32:53:50:41").isEqualTo(OK);
         assertMessageValidity("0F:32:45:4E:47:8C:49:D3:48").isEqualTo(OK);
 
-        assertMessageValidity("40:32:53:50:41").isEqualTo(ERROR_DESTINATION);
-        assertMessageValidity("F0:32").isEqualTo(ERROR_SOURCE);
-        assertMessageValidity("4F:32:45:55").isEqualTo(ERROR_PARAMETER_SHORT);
-        assertMessageValidity("4F:32:19:7F:83").isEqualTo(ERROR_PARAMETER);
+        assertMessageValidity("04:32:53:50:41").isEqualTo(ERROR_DESTINATION);
+        assertMessageValidity("40:32").isEqualTo(ERROR_SOURCE);
+        assertMessageValidity("0F:32:45:55").isEqualTo(ERROR_PARAMETER_SHORT);
+        assertMessageValidity("0F:32:19:7F:83").isEqualTo(ERROR_PARAMETER);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index e0ef035..a6f2196 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -30,6 +30,7 @@
 import android.app.role.RoleManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
 import android.os.Binder;
 import android.os.BugreportManager.BugreportCallback;
 import android.os.BugreportParams;
@@ -37,6 +38,7 @@
 import android.os.IDumpstateListener;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -67,6 +69,9 @@
 @RunWith(AndroidJUnit4.class)
 public class BugreportManagerServiceImplTest {
 
+    private static final UserInfo ADMIN_USER_INFO =
+            new UserInfo(/* id= */ 5678, "adminUser", UserInfo.FLAG_ADMIN);
+
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -82,6 +87,8 @@
     @Mock
     private DevicePolicyManager mMockDevicePolicyManager;
 
+    private TestInjector mInjector;
+
     private int mCallingUid = 1234;
     private String mCallingPackage  = "test.package";
     private AtomicFile mMappingFile;
@@ -96,9 +103,9 @@
         mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml");
         ArraySet<String> mAllowlistedPackages = new ArraySet<>();
         mAllowlistedPackages.add(mContext.getPackageName());
-        mService = new BugreportManagerServiceImpl(
-                new TestInjector(mContext, mAllowlistedPackages, mMappingFile,
-                        mMockUserManager, mMockDevicePolicyManager));
+        mInjector = new TestInjector(mContext, mAllowlistedPackages, mMappingFile,
+                mMockUserManager, mMockDevicePolicyManager);
+        mService = new BugreportManagerServiceImpl(mInjector);
         mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile);
         when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid);
         // The calling user is an admin user by default.
@@ -190,6 +197,33 @@
     }
 
     @Test
+    public void testStartBugreport() throws Exception {
+        mService.startBugreport(mCallingUid, mContext.getPackageName(),
+                new FileDescriptor(), /* screenshotFd= */ null,
+                BugreportParams.BUGREPORT_MODE_FULL,
+                /* flags= */ 0, new Listener(new CountDownLatch(1)),
+                /* isScreenshotRequested= */ false);
+
+        assertThat(mInjector.isBugreportStarted()).isTrue();
+    }
+
+    @Test
+    public void testStartBugreport_nonAdminProfileOfAdminCurrentUser() throws Exception {
+        int callingUid = Binder.getCallingUid();
+        int callingUserId = UserHandle.getUserId(callingUid);
+        when(mMockUserManager.isUserAdmin(callingUserId)).thenReturn(false);
+        when(mMockUserManager.getProfileParent(callingUserId)).thenReturn(ADMIN_USER_INFO);
+
+        mService.startBugreport(mCallingUid, mContext.getPackageName(),
+                new FileDescriptor(), /* screenshotFd= */ null,
+                BugreportParams.BUGREPORT_MODE_FULL,
+                /* flags= */ 0, new Listener(new CountDownLatch(1)),
+                /* isScreenshotRequested= */ false);
+
+        assertThat(mInjector.isBugreportStarted()).isTrue();
+    }
+
+    @Test
     public void testStartBugreport_throwsForNonAdminUser() throws Exception {
         when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false);
 
@@ -317,8 +351,12 @@
 
     private static class TestInjector extends BugreportManagerServiceImpl.Injector {
 
+        private static final String SYSTEM_PROPERTY_BUGREPORT_START = "ctl.start";
+        private static final String SYSTEM_PROPERTY_BUGREPORT_STOP = "ctl.stop";
+
         private final UserManager mUserManager;
         private final DevicePolicyManager mDevicePolicyManager;
+        private boolean mBugreportStarted = false;
 
         TestInjector(Context context, ArraySet<String> allowlistedPackages, AtomicFile mappingFile,
                 UserManager um, DevicePolicyManager dpm) {
@@ -336,5 +374,20 @@
         public DevicePolicyManager getDevicePolicyManager() {
             return mDevicePolicyManager;
         }
+
+        @Override
+        public void setSystemProperty(String key, String value) {
+            // Calling SystemProperties.set() will throw a RuntimeException due to permission error.
+            // Instead, we are just marking a flag to store the state for testing.
+            if (SYSTEM_PROPERTY_BUGREPORT_START.equals(key)) {
+                mBugreportStarted = true;
+            } else if (SYSTEM_PROPERTY_BUGREPORT_STOP.equals(key)) {
+                mBugreportStarted = false;
+            }
+        }
+
+        public boolean isBugreportStarted() {
+            return mBugreportStarted;
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 507b3fe..1591a96 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -316,6 +316,10 @@
                 .that(userTypeDetails).isNotNull();
         final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
 
+        // Only run the test if private profile creation is enabled on the device
+        assumeTrue("Private profile not enabled on the device",
+                mUserManager.canAddPrivateProfile());
+
         // Test that only one private profile  can be created
         final int mainUserId = mainUser.getIdentifier();
         UserInfo userInfo = createProfileForUser("Private profile1",
@@ -1231,6 +1235,20 @@
 
     @MediumTest
     @Test
+    public void testPrivateProfileCreationRestrictions() {
+        assumeTrue(mUserManager.canAddPrivateProfile());
+        final int mainUserId = ActivityManager.getCurrentUser();
+        try {
+            UserInfo privateProfileInfo = createProfileForUser("Private",
+                            UserManager.USER_TYPE_PROFILE_PRIVATE, mainUserId);
+            assertThat(privateProfileInfo).isNotNull();
+        } catch (Exception e) {
+            fail("Creation of private profile failed due to " + e.getMessage());
+        }
+    }
+
+    @MediumTest
+    @Test
     public void testAddRestrictedProfile() throws Exception {
         if (isAutomotive() || UserManager.isHeadlessSystemUserMode()) return;
         assertWithMessage("There should be no associated restricted profiles before the test")
diff --git a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
index b9ece93..e27bb4c 100644
--- a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
+++ b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
@@ -40,19 +40,12 @@
 public class StubTransaction extends SurfaceControl.Transaction {
 
     private HashSet<Runnable> mWindowInfosReportedListeners = new HashSet<>();
-    private HashSet<SurfaceControl.TransactionCommittedListener> mTransactionCommittedListeners =
-            new HashSet<>();
 
     @Override
     public void apply() {
         for (Runnable listener : mWindowInfosReportedListeners) {
             listener.run();
         }
-        for (SurfaceControl.TransactionCommittedListener listener
-                : mTransactionCommittedListeners) {
-            listener.onTransactionCommitted();
-        }
-        mTransactionCommittedListeners.clear();
     }
 
     @Override
@@ -246,9 +239,6 @@
     @Override
     public SurfaceControl.Transaction addTransactionCommittedListener(Executor executor,
             SurfaceControl.TransactionCommittedListener listener) {
-        SurfaceControl.TransactionCommittedListener listenerInner =
-                () -> executor.execute(listener::onTransactionCommitted);
-        mTransactionCommittedListeners.add(listenerInner);
         return this;
     }
 
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index 2f29d10..515898a 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -48,6 +48,8 @@
         "notification_flags_lib",
         "platform-test-rules",
         "SettingsLib",
+        "libprotobuf-java-lite",
+        "platformprotoslite",
     ],
 
     libs: [
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 4dded1d..05b6c90 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -885,6 +886,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -915,6 +917,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -945,6 +948,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a/a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -975,6 +979,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a/a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -1152,6 +1157,58 @@
     }
 
     @Test
+    public void testUpgradeAppNoPermissionNoRebind() throws Exception {
+        Context context = spy(getContext());
+        doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+                mIpm,
+                APPROVAL_BY_COMPONENT);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        addExpectedServices(service, packages, 0);
+
+        final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+        final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+        // Both components are approved initially
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryPackages.clear();
+
+        loadXml(service);
+
+        //Component package/C1 loses bind permission
+        when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+                (Answer<ServiceInfo>) invocation -> {
+                    ComponentName invocationCn = invocation.getArgument(0);
+                    if (invocationCn != null) {
+                        ServiceInfo serviceInfo = new ServiceInfo();
+                        serviceInfo.packageName = invocationCn.getPackageName();
+                        serviceInfo.name = invocationCn.getClassName();
+                        if (invocationCn.equals(unapprovedComponent)) {
+                            serviceInfo.permission = "none";
+                        } else {
+                            serviceInfo.permission = service.getConfig().bindPermission;
+                        }
+                        serviceInfo.metaData = null;
+                        return serviceInfo;
+                    }
+                    return null;
+                }
+        );
+
+        // Trigger package update
+        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+        assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent));
+        assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent));
+    }
+
+    @Test
     public void testSetPackageOrComponentEnabled() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index 33ca5c2..a45b102 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -20,6 +20,7 @@
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 
+import static junit.framework.Assert.fail;
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
@@ -32,6 +33,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -47,9 +49,12 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Xml;
+import android.Manifest;
 
+import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.function.TriPredicate;
 import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.NotificationManagerService.NotificationAssistants;
 
@@ -59,7 +64,9 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -89,11 +96,15 @@
     UserInfo mZero = new UserInfo(0, "zero", 0);
     UserInfo mTen = new UserInfo(10, "ten", 0);
 
+    ComponentName mCn = new ComponentName("a", "b");
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mContext.setMockPackageManager(mPm);
         mContext.addMockSystemService(Context.USER_SERVICE, mUm);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.string.config_defaultAssistantAccessComponent, "a/a");
         mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm));
         when(mNm.getBinderService()).thenReturn(mINm);
         mContext.ensureTestableResources();
@@ -102,8 +113,9 @@
         ResolveInfo resolve = new ResolveInfo();
         approved.add(resolve);
         ServiceInfo info = new ServiceInfo();
-        info.packageName = "a";
-        info.name="a";
+        info.packageName = mCn.getPackageName();
+        info.name = mCn.getClassName();
+        info.permission = Manifest.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE;
         resolve.serviceInfo = info;
         when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt()))
                 .thenReturn(approved);
@@ -137,6 +149,51 @@
     }
 
     @Test
+    public void testWriteXml_userTurnedOffNAS() throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+
+        mAssistants.loadDefaultsFromConfig(true);
+
+        mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+               true, true);
+
+        ComponentName current = CollectionUtils.firstOrNull(
+                mAssistants.getAllowedComponents(userId));
+        assertNotNull(current);
+        mAssistants.setUserSet(userId, true);
+        mAssistants.setPackageOrComponentEnabled(current.flattenToString(), userId, true, false,
+                true);
+
+        TypedXmlSerializer serializer = Xml.newFastSerializer();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+        serializer.startDocument(null, true);
+        mAssistants.writeXml(serializer, true, userId);
+        serializer.endDocument();
+        serializer.flush();
+
+        //fail(baos.toString("UTF-8"));
+
+        final TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        TriPredicate<String, Integer, String> allowedManagedServicePackages =
+                mNm::canUseManagedServices;
+
+        parser.nextTag();
+        mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm));
+        mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+
+        ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
+        // approved should not be null
+        assertNotNull(approved);
+        assertEquals(new ArraySet<>(), approved.get(true));
+
+        // user set is maintained
+        assertTrue(mAssistants.mIsUserChanged.get(ActivityManager.getCurrentUser()));
+    }
+
+    @Test
     public void testReadXml_userDisabled() throws Exception {
         String xml = "<enabled_assistants version=\"4\" defaults=\"b/b\">"
                 + "<service_listing approved=\"\" user=\"0\" primary=\"true\""
@@ -160,6 +217,33 @@
     }
 
     @Test
+    public void testReadXml_userDisabled_restore() throws Exception {
+        String xml = "<enabled_assistants version=\"4\" defaults=\"b/b\">"
+                + "<service_listing approved=\"\" user=\"0\" primary=\"true\""
+                + "user_changed=\"true\"/>"
+                + "</enabled_assistants>";
+
+        final TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(xml.toString().getBytes())), null);
+        TriPredicate<String, Integer, String> allowedManagedServicePackages =
+                mNm::canUseManagedServices;
+
+        parser.nextTag();
+        mAssistants.readXml(parser, allowedManagedServicePackages, true,
+                ActivityManager.getCurrentUser());
+
+        ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
+
+        // approved should not be null
+        assertNotNull(approved);
+        assertEquals(new ArraySet<>(), approved.get(true));
+
+        // user set is maintained
+        assertTrue(mAssistants.mIsUserChanged.get(ActivityManager.getCurrentUser()));
+    }
+
+    @Test
     public void testReadXml_upgradeUserSet() throws Exception {
         String xml = "<enabled_assistants version=\"3\" defaults=\"b/b\">"
                 + "<service_listing approved=\"\" user=\"0\" primary=\"true\""
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java
index 3499a12..bf8cfa5c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java
@@ -20,8 +20,12 @@
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.TestCase.assertFalse;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -70,10 +74,11 @@
 
     @Before
     public void setUp() throws Exception {
-        mJobService = new NotificationHistoryJobService();
+        mJobService = spy(new NotificationHistoryJobService());
         mJobService.attachBaseContext(mContext);
         mJobService.onCreate();
         mJobService.onBind(/* intent= */ null);  // Create JobServiceEngine within JobService.
+        doNothing().when(mJobService).jobFinished(any(), eq(false));
 
         mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index e3ea55a..03f2749 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -14092,7 +14092,8 @@
 
     @Test
     public void testProfileUnavailableIntent() throws RemoteException {
-        mSetFlagsRule.enableFlags(FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.enableFlags(FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         simulateProfileAvailabilityActions(Intent.ACTION_PROFILE_UNAVAILABLE);
         verify(mWorkerHandler).post(any(Runnable.class));
         verify(mSnoozeHelper).clearData(anyInt());
@@ -14101,7 +14102,8 @@
 
     @Test
     public void testManagedProfileUnavailableIntent() throws RemoteException {
-        mSetFlagsRule.disableFlags(FLAG_ALLOW_PRIVATE_PROFILE);
+        mSetFlagsRule.disableFlags(FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
         simulateProfileAvailabilityActions(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
         verify(mWorkerHandler).post(any(Runnable.class));
         verify(mSnoozeHelper).clearData(anyInt());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 78fb570..bfc47fd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -324,7 +324,11 @@
         when(mPermissionHelper.getNotificationPermissionValues(USER_SYSTEM))
                 .thenReturn(appPermissions);
 
-        when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0}));
+        IntArray currentProfileIds = IntArray.wrap(new int[]{0});
+        if (UserManager.isHeadlessSystemUserMode()) {
+            currentProfileIds.add(UserHandle.getUserId(UID_HEADLESS));
+        }
+        when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds);
 
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
index 99d5a6d..75552bc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.notification;
 
-import static com.android.server.notification.ZenAdapters.notificationPolicyToZenPolicy;
+import static android.service.notification.ZenAdapters.notificationPolicyToZenPolicy;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
new file mode 100644
index 0000000..f724510
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.provider.Settings;
+import android.service.notification.ZenPolicy;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.os.dnd.ActiveRuleType;
+import com.android.os.dnd.ChannelPolicy;
+import com.android.os.dnd.ConversationType;
+import com.android.os.dnd.PeopleType;
+import com.android.os.dnd.State;
+import com.android.os.dnd.ZenMode;
+
+import com.google.protobuf.Internal;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Test to validate that logging enums used in Zen classes match their API definitions. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ZenEnumTest {
+
+    @Test
+    public void testEnum_zenMode() {
+        testEnum(Settings.Global.class, "ZEN_MODE", ZenMode.class, "ZEN_MODE");
+    }
+
+    @Test
+    public void testEnum_activeRuleType() {
+        testEnum(AutomaticZenRule.class, "TYPE", ActiveRuleType.class, "TYPE");
+    }
+
+    @Test
+    public void testEnum_zenPolicyState() {
+        testEnum(ZenPolicy.class, "STATE", State.class, "STATE");
+    }
+
+    @Test
+    public void testEnum_zenPolicyChannelPolicy() {
+        testEnum(ZenPolicy.class, "CHANNEL_POLICY", ChannelPolicy.class, "CHANNEL_POLICY");
+    }
+
+    @Test
+    public void testEnum_zenPolicyConversationType() {
+        testEnum(ZenPolicy.class, "CONVERSATION_SENDERS", ConversationType.class, "CONV");
+    }
+
+    @Test
+    public void testEnum_zenPolicyPeopleType() {
+        testEnum(ZenPolicy.class, "PEOPLE_TYPE", PeopleType.class, "PEOPLE");
+    }
+
+    /**
+     * Verifies that any constants (i.e. {@code public static final int} fields) named {@code
+     * <apiPrefix>_SOMETHING} in {@code apiClass} are present and have the same numerical value
+     * in the enum values defined in {@code loggingProtoEnumClass}.
+     *
+     * <p>Note that <em>extra</em> values in the logging enum are accepted (since we have one of
+     * those, and the main goal of this test is that we don't forget to update the logging enum
+     * if new API enum values are added).
+     */
+    private static void testEnum(Class<?> apiClass, String apiPrefix,
+            Class<? extends Internal.EnumLite> loggingProtoEnumClass,
+            String loggingPrefix) {
+        Map<String, Integer> apiConstants =
+                Arrays.stream(apiClass.getDeclaredFields())
+                        .filter(f -> Modifier.isPublic(f.getModifiers()))
+                        .filter(f -> Modifier.isStatic(f.getModifiers()))
+                        .filter(f -> Modifier.isFinal(f.getModifiers()))
+                        .filter(f -> f.getType().equals(int.class))
+                        .filter(f -> f.getName().startsWith(apiPrefix + "_"))
+                        .collect(Collectors.toMap(
+                                Field::getName,
+                                ZenEnumTest::getStaticFieldIntValue));
+
+        Map<String, Integer> loggingConstants =
+                Arrays.stream(loggingProtoEnumClass.getEnumConstants())
+                        .collect(Collectors.toMap(
+                                v -> v.toString(),
+                                v -> v.getNumber()));
+
+        Map<String, Integer> renamedApiConstants = apiConstants.entrySet().stream()
+                .collect(Collectors.toMap(
+                        entry -> entry.getKey().replace(apiPrefix + "_", loggingPrefix + "_"),
+                        Map.Entry::getValue));
+
+        assertThat(loggingConstants).containsAtLeastEntriesIn(renamedApiConstants);
+    }
+
+    private static int getStaticFieldIntValue(Field f) {
+        try {
+            return f.getInt(null);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 495e01a..c4d2460 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -136,6 +136,7 @@
 import android.provider.Settings.Global;
 import android.service.notification.Condition;
 import android.service.notification.DeviceEffectsApplier;
+import android.service.notification.ZenAdapters;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
@@ -4807,6 +4808,53 @@
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_ruleChanged_deactivatesRule() {
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
+                .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, UPDATE_ORIGIN_APP, "reason",
+                CUSTOM_PKG_UID);
+        mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+        AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule)
+                .setTriggerDescription("Whenever")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_APP, "reason",
+                CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isNull();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_ruleNotChanged_doesNotDeactivateRule() {
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID)
+                .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, UPDATE_ORIGIN_APP, "reason",
+                CUSTOM_PKG_UID);
+        mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+        AutomaticZenRule updateUnchanged = new AutomaticZenRule.Builder(rule).build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, updateUnchanged, UPDATE_ORIGIN_APP, "reason",
+                CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo(
+                CONDITION_TRUE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
     public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() {
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
         reset(mDeviceEffectsApplier);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java
index 038f1db..54ab367 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java
@@ -135,7 +135,7 @@
         AggregatedLogRecord<TestSingleLogRecord> droppedRecord =
                 records.add(createRecord(GROUP_1, KEY_2, createTime++));
         assertThat(droppedRecord).isNotNull();
-        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.getFirst());
+        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.get(0));
 
         dumpRecords(records);
         assertGroupHeadersWrittenOnce(records, GROUP_1);
@@ -155,7 +155,7 @@
         AggregatedLogRecord<TestSingleLogRecord> droppedRecord =
                 records.add(createRecord(GROUP_1, KEY_1, createTime + AGGREGATION_TIME_LIMIT));
         assertThat(droppedRecord).isNotNull();
-        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.getFirst());
+        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.get(0));
 
         dumpRecords(records);
         assertGroupHeadersWrittenOnce(records, GROUP_1);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index f54c7e5..88a9483 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -604,7 +604,8 @@
     @RequiresFlagsEnabled(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED)
     public void shouldIgnoreVibration_withKeyboardSettingsOff_shouldIgnoreKeyboardVibration() {
         setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
-        setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 0);
+        setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 0 /* OFF*/);
+        setHasFixedKeyboardAmplitudeIntensity(true);
 
         // Keyboard touch ignored.
         assertVibrationIgnoredForAttributes(
@@ -628,7 +629,8 @@
     @RequiresFlagsEnabled(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED)
     public void shouldIgnoreVibration_withKeyboardSettingsOn_shouldNotIgnoreKeyboardVibration() {
         setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
-        setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1);
+        setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */);
+        setHasFixedKeyboardAmplitudeIntensity(true);
 
         // General touch ignored.
         assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS);
@@ -642,6 +644,25 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED)
+    public void shouldIgnoreVibration_noFixedKeyboardAmplitude_ignoresKeyboardTouchVibration() {
+        setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+        setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */);
+        setHasFixedKeyboardAmplitudeIntensity(false);
+
+        // General touch ignored.
+        assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS);
+
+        // Keyboard touch ignored.
+        assertVibrationIgnoredForAttributes(
+                new VibrationAttributes.Builder()
+                        .setUsage(USAGE_TOUCH)
+                        .setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
+                        .build(),
+                Vibration.Status.IGNORED_FOR_SETTINGS);
+    }
+
+    @Test
     public void shouldIgnoreVibrationFromVirtualDevices_defaultDevice_neverIgnored() {
         // Vibrations from the primary device is never ignored.
         for (int usage : ALL_USAGES) {
@@ -953,6 +974,10 @@
         when(mVibrationConfigMock.ignoreVibrationsOnWirelessCharger()).thenReturn(ignore);
     }
 
+    private void setHasFixedKeyboardAmplitudeIntensity(boolean hasFixedAmplitude) {
+        when(mVibrationConfigMock.hasFixedKeyboardAmplitude()).thenReturn(hasFixedAmplitude);
+    }
+
     private void deleteUserSetting(String settingName) {
         Settings.System.putStringForUser(
                 mContextSpy.getContentResolver(), settingName, null, UserHandle.USER_CURRENT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 09e7b91..45a2ba4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3147,12 +3147,12 @@
         // By default, activity is visible.
         assertTrue(activity.isVisible());
         assertTrue(activity.isVisibleRequested());
-        assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
         // Request the activity to be visible. Although the activity is already visible, app
         // transition animation should be applied on this activity. This might be unnecessary, but
         // until we verify no logic relies on this behavior, we'll keep this as is.
+        mDisplayContent.prepareAppTransition(0);
         activity.setVisibility(true);
         assertTrue(activity.isVisible());
         assertTrue(activity.isVisibleRequested());
@@ -3167,11 +3167,11 @@
         // By default, activity is visible.
         assertTrue(activity.isVisible());
         assertTrue(activity.isVisibleRequested());
-        assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
         // Request the activity to be invisible. Since the visibility changes, app transition
         // animation should be applied on this activity.
+        mDisplayContent.prepareAppTransition(0);
         activity.setVisibility(false);
         assertTrue(activity.isVisible());
         assertFalse(activity.isVisibleRequested());
@@ -3187,7 +3187,6 @@
         // activity.
         assertFalse(activity.isVisible());
         assertTrue(activity.isVisibleRequested());
-        assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
         // Request the activity to be visible. Since the visibility changes, app transition
@@ -3389,6 +3388,7 @@
         // frozen until the input started.
         mDisplayContent.setImeLayeringTarget(app1);
         mDisplayContent.updateImeInputAndControlTarget(app1);
+        mDisplayContent.computeImeTarget(true /* updateImeTarget */);
         performSurfacePlacementAndWaitForWindowAnimator();
 
         assertEquals(app1, mDisplayContent.getImeInputTarget());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 1a1fe95..0c1fbf3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -94,7 +94,6 @@
     public void setUp() throws Exception {
         assumeFalse(WindowManagerService.sEnableShellTransitions);
         mAppTransitionController = new AppTransitionController(mWm, mDisplayContent);
-        mWm.mAnimator.ready();
     }
 
     @Test
@@ -856,7 +855,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -887,7 +886,7 @@
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity,
                 null /* changingTaskFragment */);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation is not run by the remote handler because the activity is filling the Task.
         assertFalse(remoteAnimationRunner.isAnimationStarted());
@@ -922,7 +921,7 @@
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity,
                 null /* changingTaskFragment */);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -947,7 +946,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -974,7 +973,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -998,7 +997,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation not run by the remote handler.
         assertFalse(remoteAnimationRunner.isAnimationStarted());
@@ -1025,7 +1024,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation should not run by the remote handler when there are non-embedded activities of
         // different UID.
@@ -1052,7 +1051,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation should not run by the remote handler when there is wallpaper in the transition.
         assertFalse(remoteAnimationRunner.isAnimationStarted());
@@ -1086,7 +1085,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity1, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // The animation will be animated remotely by client and all activities are input disabled
         // for untrusted animation.
@@ -1137,7 +1136,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // The animation will be animated remotely by client and all activities are input disabled
         // for untrusted animation.
@@ -1179,7 +1178,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // The animation will be animated remotely by client, but input should not be dropped for
         // fully trusted.
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 1f15ec3..2085d61 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -371,7 +371,6 @@
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(app);
         mDisplayContent.getInsetsPolicy().showTransient(statusBars(),
                 true /* isGestureOnSystemBar */);
-        mWm.mAnimator.ready();
         waitUntilWindowAnimatorIdle();
 
         assertTrue(mDisplayContent.getInsetsPolicy().isTransient(statusBars()));
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index a163801..11d9629 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -43,6 +43,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -112,7 +113,6 @@
         runWithScissors(mWm.mH, () -> mHandler = new TestHandler(null, mClock), 0);
         mController = new RemoteAnimationController(mWm, mDisplayContent, mAdapter,
                 mHandler, false /*isActivityEmbedding*/);
-        mWm.mAnimator.ready();
     }
 
     private WindowState createAppOverlayWindow() {
@@ -136,7 +136,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -168,7 +168,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -290,7 +290,7 @@
         adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                 mFinishedCallback);
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
         final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                 ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
         final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -336,7 +336,7 @@
         task.applyAnimationUnchecked(null /* lp */, true /* enter */, TRANSIT_OLD_TASK_OPEN,
                 false /* isVoiceInteraction */, null /* sources */);
         mController.goodToGo(TRANSIT_OLD_TASK_OPEN);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
         final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                 ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
         try {
@@ -363,7 +363,7 @@
             ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
                     mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -417,7 +417,7 @@
             ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
                     mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -471,7 +471,7 @@
             ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
                     mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -526,7 +526,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -559,7 +559,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -595,7 +595,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -645,7 +645,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -782,7 +782,7 @@
 
             mDisplayContent.applySurfaceChangesTransaction();
             mController.goodToGo(TRANSIT_OLD_TASK_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
             verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_OPEN),
                     any(), any(), any(), any());
@@ -810,7 +810,7 @@
         adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                 mFinishedCallback);
         mController.goodToGo(transit);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
         return adapter;
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index 527ea0d..ce90504 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -1239,7 +1239,7 @@
         final ActivityRecord activity1 = finishTopActivity(rootTask1);
         assertEquals(DESTROYING, activity1.getState());
         verify(mRootWindowContainer).ensureVisibilityAndConfig(eq(null) /* starting */,
-                eq(display.mDisplayId), anyBoolean());
+                eq(display), anyBoolean());
     }
 
     private ActivityRecord finishTopActivity(Task task) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 6013063..649f520 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -554,7 +554,7 @@
         mRootWindowContainer.applySleepTokens(true);
 
         // The display orientation should be changed by the activity so there is no relaunch.
-        verify(activity, never()).relaunchActivityLocked(anyBoolean());
+        verify(activity, never()).relaunchActivityLocked(anyBoolean(), anyInt());
         assertEquals(rotatedConfig.orientation, display.getConfiguration().orientation);
     }
 
@@ -835,8 +835,6 @@
                 new TestDisplayContent.Builder(mAtm, 1000, 1500)
                         .setSystemDecorations(true).build();
 
-        doReturn(true).when(mRootWindowContainer)
-                .ensureVisibilityAndConfig(any(), anyInt(), anyBoolean());
         doReturn(true).when(mRootWindowContainer).canStartHomeOnDisplayArea(any(), any(),
                 anyBoolean());
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 7ab093d..a8f6fe8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -546,7 +546,7 @@
         // This makes sure all previous messages in the handler are fully processed vs. just popping
         // them from the message queue.
         final AtomicBoolean currentMessagesProcessed = new AtomicBoolean(false);
-        wm.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
+        wm.mAnimator.getChoreographer().postFrameCallback(time -> {
             synchronized (currentMessagesProcessed) {
                 currentMessagesProcessed.set(true);
                 currentMessagesProcessed.notifyAll();
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 01bd96b..5360a10 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -896,6 +896,11 @@
             assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop));
             // The focus should NOT change.
             assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+
+            // Do not move focus if the dim is boosted.
+            taskFragmentLeft.mDimmerSurfaceBoosted = true;
+            assertFalse(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
+            assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
         }
     }
 
diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java
index 8b44579..36adeec 100644
--- a/services/usb/java/com/android/server/usb/UsbHostManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHostManager.java
@@ -380,6 +380,11 @@
             return false;
         }
 
+        if (descriptors == null) {
+            Slog.e(TAG, "Failed to add device as the descriptor is null");
+            return false;
+        }
+
         UsbDescriptorParser parser = new UsbDescriptorParser(deviceAddress, descriptors);
         if (deviceClass == UsbConstants.USB_CLASS_PER_INTERFACE
                 && !checkUsbInterfacesDenyListed(parser)) {
@@ -462,8 +467,7 @@
                 }
 
                 // Tracking
-                addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT,
-                        parser.getRawDescriptors());
+                addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT, descriptors);
 
                 // Stats collection
                 FrameworkStatsLog.write(FrameworkStatsLog.USB_DEVICE_ATTACHED,
diff --git a/telecomm/java/android/telecom/RemoteConnectionService.java b/telecomm/java/android/telecom/RemoteConnectionService.java
index 2fc6a22..8b503f2 100644
--- a/telecomm/java/android/telecom/RemoteConnectionService.java
+++ b/telecomm/java/android/telecom/RemoteConnectionService.java
@@ -28,6 +28,8 @@
 import com.android.internal.telecom.IConnectionServiceAdapter;
 import com.android.internal.telecom.IVideoProvider;
 import com.android.internal.telecom.RemoteServiceCallback;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -550,6 +552,9 @@
     private final Map<String, RemoteConference> mConferenceById = new HashMap<>();
     private final Set<RemoteConnection> mPendingConnections = new HashSet<>();
 
+    /** Telecom feature flags **/
+    private final FeatureFlags mTelecomFeatureFlags = new FeatureFlagsImpl();
+
     RemoteConnectionService(
             IConnectionService outgoingConnectionServiceRpc,
             ConnectionService ourConnectionServiceImpl) throws RemoteException {
@@ -578,6 +583,14 @@
         extras.putString(Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME,
                 mOurConnectionServiceImpl.getApplicationContext().getOpPackageName());
 
+        // Defaulted ConnectionRequest params
+        String telecomCallId = "";
+        boolean shouldShowIncomingUI = false;
+        if (mTelecomFeatureFlags.setRemoteConnectionCallId()) {
+            telecomCallId = id;
+            shouldShowIncomingUI = request.shouldShowIncomingCallUi();
+        }
+
         final ConnectionRequest newRequest = new ConnectionRequest.Builder()
                 .setAccountHandle(request.getAccountHandle())
                 .setAddress(request.getAddress())
@@ -585,6 +598,9 @@
                 .setVideoState(request.getVideoState())
                 .setRttPipeFromInCall(request.getRttPipeFromInCall())
                 .setRttPipeToInCall(request.getRttPipeToInCall())
+                // Flagged changes
+                .setTelecomCallId(telecomCallId)
+                .setShouldShowIncomingCallUi(shouldShowIncomingUI)
                 .build();
         try {
             if (mConnectionById.isEmpty()) {
@@ -626,10 +642,28 @@
                 mOutgoingConnectionServiceRpc.addConnectionServiceAdapter(mServant.getStub(),
                         null /*Session.Info*/);
             }
+
+            // Set telecom call id to what's being tracked by base ConnectionService.
+            String telecomCallId = mTelecomFeatureFlags.setRemoteConnectionCallId()
+                    ? id : request.getTelecomCallId();
+
+            final ConnectionRequest newRequest = new ConnectionRequest.Builder()
+                    .setAccountHandle(request.getAccountHandle())
+                    .setAddress(request.getAddress())
+                    .setExtras(request.getExtras())
+                    .setVideoState(request.getVideoState())
+                    .setShouldShowIncomingCallUi(request.shouldShowIncomingCallUi())
+                    .setRttPipeFromInCall(request.getRttPipeFromInCall())
+                    .setRttPipeToInCall(request.getRttPipeToInCall())
+                    .setParticipants(request.getParticipants())
+                    .setIsAdhocConferenceCall(request.isAdhocConferenceCall())
+                    .setTelecomCallId(telecomCallId)
+                    .build();
+
             RemoteConference conference = new RemoteConference(id, mOutgoingConnectionServiceRpc);
             mOutgoingConnectionServiceRpc.createConference(connectionManagerPhoneAccount,
                     id,
-                    request,
+                    newRequest,
                     isIncoming,
                     false /* isUnknownCall */,
                     null /*Session.info*/);
@@ -640,7 +674,7 @@
                     maybeDisconnectAdapter();
                 }
             });
-            conference.putExtras(request.getExtras());
+            conference.putExtras(newRequest.getExtras());
             return conference;
         } catch (RemoteException e) {
             return RemoteConference.failure(
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 5d99acd..2150b5d 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -5307,6 +5307,19 @@
                 KEY_PREFIX + "enable_presence_group_subscribe_bool";
 
         /**
+         * SIP SUBSCRIBE retry duration used when device doesn't receive a response to SIP
+         * SUBSCRIBE request.
+         * If this value is not defined or defined as negative value, the device does not retry
+         * the SIP SUBSCRIBE.
+         * If the value is 0 then device retries immediately upon timeout.
+         * If the value is > 0 then device waits for configured duration and retries after timeout
+         * is detected
+         * @hide
+         */
+        public static final String KEY_SUBSCRIBE_RETRY_DURATION_MILLIS_LONG =
+                KEY_PREFIX + "subscribe_retry_duration_millis_long";
+
+        /**
          * Flag indicating whether or not to use SIP URI when send a presence subscribe.
          * When {@code true}, the device sets the To and Contact header to be SIP URI using
          * the TelephonyManager#getIsimDomain" API.
@@ -5982,6 +5995,7 @@
             defaults.putBoolean(KEY_ENABLE_PRESENCE_CAPABILITY_EXCHANGE_BOOL, false);
             defaults.putBoolean(KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, false);
             defaults.putBoolean(KEY_ENABLE_PRESENCE_GROUP_SUBSCRIBE_BOOL, false);
+            defaults.putInt(KEY_SUBSCRIBE_RETRY_DURATION_MILLIS_LONG, -1);
             defaults.putBoolean(KEY_USE_SIP_URI_FOR_PRESENCE_SUBSCRIBE_BOOL, false);
             defaults.putInt(KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT, 30 * 24 * 60 * 60);
             defaults.putBoolean(KEY_RCS_REQUEST_FORBIDDEN_BY_SIP_489_BOOL, false);
diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java
index b84ff29..12e04c2 100644
--- a/telephony/java/android/telephony/ims/ImsService.java
+++ b/telephony/java/android/telephony/ims/ImsService.java
@@ -195,7 +195,8 @@
                     // whether or not ImsFeature.FEATURE_EMERGENCY_MMTEL feature is set and should
                     // not be set by users of ImsService.
                     CAPABILITY_SIP_DELEGATE_CREATION,
-                    CAPABILITY_TERMINAL_BASED_CALL_WAITING
+                    CAPABILITY_TERMINAL_BASED_CALL_WAITING,
+                    CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ImsServiceCapability {}
@@ -206,7 +207,9 @@
      */
     private static final Map<Long, String> CAPABILITIES_LOG_MAP = Map.of(
             CAPABILITY_EMERGENCY_OVER_MMTEL, "EMERGENCY_OVER_MMTEL",
-            CAPABILITY_SIP_DELEGATE_CREATION, "SIP_DELEGATE_CREATION");
+            CAPABILITY_SIP_DELEGATE_CREATION, "SIP_DELEGATE_CREATION",
+            CAPABILITY_TERMINAL_BASED_CALL_WAITING, "TERMINAL_BASED_CALL_WAITING",
+            CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING, "SIMULTANEOUS_CALLING");
 
     /**
      * The intent that must be defined as an intent-filter in the AndroidManifest of the ImsService.
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 1d71f95..d658d59 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -63,17 +63,20 @@
     ],
 }
 
-android_library_import {
-    name: "wm-flicker-window-extensions_nodeps",
-    aars: ["libs/window-extensions-release.aar"],
-    sdk_version: "current",
-}
-
 java_library {
     name: "wm-flicker-window-extensions",
     sdk_version: "current",
     static_libs: [
-        "wm-flicker-window-extensions_nodeps",
+        "androidx.window.extensions_extensions-nodeps",
+    ],
+    installable: false,
+}
+
+java_library {
+    name: "wm-flicker-window-extensions-core",
+    sdk_version: "current",
+    static_libs: [
+        "androidx.window.extensions.core_core-nodeps",
     ],
     installable: false,
 }
diff --git a/tests/FlickerTests/libs/window-extensions-release.aar b/tests/FlickerTests/libs/window-extensions-release.aar
deleted file mode 100644
index 918e514..0000000
--- a/tests/FlickerTests/libs/window-extensions-release.aar
+++ /dev/null
Binary files differ
diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
index 7343ba1c..e60764f 100644
--- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -24,6 +24,7 @@
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
 import android.content.pm.ServiceInfo
+import android.hardware.input.KeyboardLayoutSelectionResult
 import android.hardware.input.IInputManager
 import android.hardware.input.InputManager
 import android.hardware.input.InputManagerGlobal
@@ -525,13 +526,13 @@
                 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
                 ENGLISH_UK_LAYOUT_DESCRIPTOR
             )
-            val keyboardLayout =
+            assertEquals(
+                "Default UI: getKeyboardLayoutForInputDevice API should always return " +
+                        "KeyboardLayoutSelectionResult.FAILED",
+                KeyboardLayoutSelectionResult.FAILED,
                 keyboardLayoutManager.getKeyboardLayoutForInputDevice(
                     keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
                 )
-            assertNull(
-                "Default UI: getKeyboardLayoutForInputDevice API should always return null",
-                keyboardLayout
             )
         }
     }
@@ -545,12 +546,14 @@
                 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
                 ENGLISH_UK_LAYOUT_DESCRIPTOR
             )
-            assertEquals(
-                "New UI: getKeyboardLayoutForInputDevice API should return the set layout",
-                ENGLISH_UK_LAYOUT_DESCRIPTOR,
+            var result =
                 keyboardLayoutManager.getKeyboardLayoutForInputDevice(
                     keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
                 )
+            assertEquals(
+                "New UI: getKeyboardLayoutForInputDevice API should return the set layout",
+                ENGLISH_UK_LAYOUT_DESCRIPTOR,
+                result.layoutDescriptor
             )
 
             // This should replace previously set layout
@@ -558,12 +561,14 @@
                 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
                 ENGLISH_US_LAYOUT_DESCRIPTOR
             )
-            assertEquals(
-                "New UI: getKeyboardLayoutForInputDevice API should return the last set layout",
-                ENGLISH_US_LAYOUT_DESCRIPTOR,
+            result =
                 keyboardLayoutManager.getKeyboardLayoutForInputDevice(
                     keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
                 )
+            assertEquals(
+                "New UI: getKeyboardLayoutForInputDevice API should return the last set layout",
+                ENGLISH_US_LAYOUT_DESCRIPTOR,
+                result.layoutDescriptor
             )
         }
     }
@@ -734,17 +739,20 @@
                 createImeSubtypeForLanguageTag("ru"),
                 createLayoutDescriptor("keyboard_layout_russian")
             )
-            assertNull(
-                "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
-                        "layout available",
+            assertEquals(
+                "New UI: getDefaultKeyboardLayoutForInputDevice should return " +
+                        "KeyboardLayoutSelectionResult.FAILED when no layout available",
+                KeyboardLayoutSelectionResult.FAILED,
                 keyboardLayoutManager.getKeyboardLayoutForInputDevice(
                     keyboardDevice.identifier, USER_ID, imeInfo,
                     createImeSubtypeForLanguageTag("it")
                 )
             )
-            assertNull(
-                "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
-                        "layout for script code is available",
+            assertEquals(
+                "New UI: getDefaultKeyboardLayoutForInputDevice should return " +
+                        "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" +
+                        "available",
+                KeyboardLayoutSelectionResult.FAILED,
                 keyboardLayoutManager.getKeyboardLayoutForInputDevice(
                     keyboardDevice.identifier, USER_ID, imeInfo,
                     createImeSubtypeForLanguageTag("en-Deva")
@@ -811,8 +819,10 @@
                 createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
                 createLayoutDescriptor("keyboard_layout_russian")
             )
-            assertNull("New UI: getDefaultKeyboardLayoutForInputDevice should return null when " +
-                    "no layout for script code is available",
+            assertEquals("New UI: getDefaultKeyboardLayoutForInputDevice should return " +
+                    "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" +
+                    "available",
+                KeyboardLayoutSelectionResult.FAILED,
                 keyboardLayoutManager.getKeyboardLayoutForInputDevice(
                     keyboardDevice.identifier, USER_ID, imeInfo,
                     createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "")
@@ -865,14 +875,16 @@
                         ArgumentMatchers.anyBoolean(),
                         ArgumentMatchers.eq(keyboardDevice.vendorId),
                         ArgumentMatchers.eq(keyboardDevice.productId),
-                        ArgumentMatchers.eq(createByteArray(
+                        ArgumentMatchers.eq(
+                            createByteArray(
                                 KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG,
                                 LAYOUT_TYPE_DEFAULT,
                                 GERMAN_LAYOUT_NAME,
-                                KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
+                                KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
                                 "de-Latn",
-                                LAYOUT_TYPE_QWERTZ),
+                                LAYOUT_TYPE_QWERTZ
                             ),
+                        ),
                         ArgumentMatchers.eq(keyboardDevice.deviceBus),
                 )
             }
@@ -893,13 +905,16 @@
                         ArgumentMatchers.anyBoolean(),
                         ArgumentMatchers.eq(englishQwertyKeyboardDevice.vendorId),
                         ArgumentMatchers.eq(englishQwertyKeyboardDevice.productId),
-                        ArgumentMatchers.eq(createByteArray(
+                        ArgumentMatchers.eq(
+                            createByteArray(
                                 "en",
                                 LAYOUT_TYPE_QWERTY,
                                 ENGLISH_US_LAYOUT_NAME,
-                                KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE,
+                                KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE,
                                 "de-Latn",
-                                LAYOUT_TYPE_QWERTZ)),
+                                LAYOUT_TYPE_QWERTZ
+                            )
+                        ),
                         ArgumentMatchers.eq(keyboardDevice.deviceBus),
                 )
             }
@@ -918,14 +933,16 @@
                         ArgumentMatchers.anyBoolean(),
                         ArgumentMatchers.eq(keyboardDevice.vendorId),
                         ArgumentMatchers.eq(keyboardDevice.productId),
-                        ArgumentMatchers.eq(createByteArray(
+                        ArgumentMatchers.eq(
+                            createByteArray(
                                 KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG,
                                 LAYOUT_TYPE_DEFAULT,
                                 "Default",
-                                KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEFAULT,
+                                KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT,
                                 KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG,
-                                LAYOUT_TYPE_DEFAULT),
+                                LAYOUT_TYPE_DEFAULT
                             ),
+                        ),
                         ArgumentMatchers.eq(keyboardDevice.deviceBus),
                 )
             }
@@ -998,12 +1015,13 @@
         imeSubtype: InputMethodSubtype,
         expectedLayout: String
     ) {
+        val result = keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+            device.identifier, USER_ID, imeInfo, imeSubtype
+        )
         assertEquals(
             "New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout",
             expectedLayout,
-            keyboardLayoutManager.getKeyboardLayoutForInputDevice(
-                device.identifier, USER_ID, imeInfo, imeSubtype
-            )
+            result.layoutDescriptor
         )
     }
 
diff --git a/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt b/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt
index 89a47b9..0615941 100644
--- a/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt
@@ -17,6 +17,7 @@
 package com.android.server.input
 
 import android.hardware.input.KeyboardLayout
+import android.hardware.input.KeyboardLayoutSelectionResult
 import android.icu.util.ULocale
 import android.platform.test.annotations.Presubmit
 import android.view.InputDevice
@@ -120,15 +121,15 @@
         val event = builder.addLayoutSelection(
             createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"),
             "English(US)(Qwerty)",
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
         ).addLayoutSelection(
             createImeSubtype(2, ULocale.forLanguageTag("en-US"), "azerty"),
             null, // Default layout type
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER
         ).addLayoutSelection(
             createImeSubtype(3, ULocale.forLanguageTag("en-US"), "qwerty"),
             "German",
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE
         ).setIsFirstTimeConfiguration(true).build()
 
         assertEquals(
@@ -158,7 +159,7 @@
             "de-CH",
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"),
             "English(US)(Qwerty)",
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
             "en-US",
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"),
         )
@@ -167,7 +168,7 @@
             "de-CH",
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"),
             KeyboardMetricsCollector.DEFAULT_LAYOUT_NAME,
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER,
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER,
             "en-US",
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"),
         )
@@ -176,7 +177,7 @@
             "de-CH",
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"),
             "German",
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE,
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE,
             "en-US",
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"),
         )
@@ -197,7 +198,7 @@
         val event = builder.addLayoutSelection(
             createImeSubtype(4, null, "qwerty"), // Default language tag
             "German",
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE
         ).build()
 
         assertExpectedLayoutConfiguration(
@@ -205,7 +206,7 @@
             KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG,
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"),
             "German",
-            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE,
+            KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE,
             KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG,
             KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"),
         )
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
index a64996c..d9a4c26 100644
--- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -59,6 +59,7 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.util.LinkedList;
+import java.util.TreeMap;
 
 /**
  * Test class for {@link ProtoLogImpl}.
@@ -89,7 +90,7 @@
         //noinspection ResultOfMethodCallIgnored
         mFile.delete();
         mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename,
-                1024 * 1024, mReader, 1024);
+                1024 * 1024, mReader, 1024, new TreeMap<>());
     }
 
     @After
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index 270f595..548adef 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -64,6 +64,7 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Random;
+import java.util.TreeMap;
 
 import perfetto.protos.Protolog;
 import perfetto.protos.ProtologCommon;
@@ -152,7 +153,8 @@
                 .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray()));
 
         mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
-        mProtoLog = new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader);
+        mProtoLog =
+                new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader, new TreeMap<>());
     }
 
     @After
diff --git a/tests/graphics/HwAccelerationTest/jni/native-lib.cpp b/tests/graphics/HwAccelerationTest/jni/native-lib.cpp
index 407d4bf..2977c21 100644
--- a/tests/graphics/HwAccelerationTest/jni/native-lib.cpp
+++ b/tests/graphics/HwAccelerationTest/jni/native-lib.cpp
@@ -30,7 +30,7 @@
 
     void setBuffer(AHardwareBuffer* buffer) {
         ASurfaceTransaction* transaction = ASurfaceTransaction_create();
-        ASurfaceTransaction_setBuffer(transaction, surfaceControl, buffer);
+        ASurfaceTransaction_setBuffer(transaction, surfaceControl, buffer, -1);
         ASurfaceTransaction_setVisibility(transaction, surfaceControl,
                                           ASURFACE_TRANSACTION_VISIBILITY_SHOW);
         ASurfaceTransaction_apply(transaction);
diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
index a359155..2690bc5 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
@@ -25,6 +25,7 @@
         const val READ_LOG_CMD = "read-log"
         private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
 
+        // TODO: This is always the same. I don't think it's required
         private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
         private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
         private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
index 1381847..837dae9 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
@@ -24,11 +24,17 @@
 import com.github.javaparser.ParserConfiguration
 import com.github.javaparser.StaticJavaParser
 import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.NodeList
 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.InitializerDeclaration
+import com.github.javaparser.ast.expr.FieldAccessExpr
 import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
 import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.ObjectCreationExpr
 import com.github.javaparser.ast.expr.SimpleName
 import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.stmt.BlockStmt
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileNotFoundException
@@ -39,8 +45,7 @@
 import java.util.concurrent.Executors
 import java.util.jar.JarOutputStream
 import java.util.zip.ZipEntry
-import kotlin.math.abs
-import kotlin.random.Random
+import kotlin.math.absoluteValue
 import kotlin.system.exitProcess
 
 object ProtoLogTool {
@@ -72,7 +77,11 @@
     }
 
     private fun processClasses(command: CommandOptions) {
-        val generationHash = abs(Random.nextInt())
+        // A deterministic hash based on the group jar path and the source files we are processing.
+        // The hash is required to make sure different ProtoLogImpls don't conflict.
+        val generationHash = (command.javaSourceArgs.toTypedArray() + command.protoLogGroupsJarArg)
+                .contentHashCode().absoluteValue
+
         // Need to generate a new impl class to inject static constants into the class.
         val generatedProtoLogImplClass =
             "com.android.internal.protolog.ProtoLogImpl_$generationHash"
@@ -93,7 +102,8 @@
         outJar.putNextEntry(zipEntry(protologImplPath))
 
         outJar.write(generateProtoLogImpl(protologImplName, command.viewerConfigFilePathArg,
-            command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath).toByteArray())
+            command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath,
+            groups, command.protoLogGroupsClassNameArg).toByteArray())
 
         val executor = newThreadPool()
 
@@ -137,6 +147,8 @@
         viewerConfigFilePath: String,
         legacyViewerConfigFilePath: String?,
         legacyOutputFilePath: String?,
+        groups: Map<String, LogGroup>,
+        protoLogGroupsClassName: String,
     ): String {
         val file = File(PROTOLOG_IMPL_SRC_PATH)
 
@@ -157,7 +169,8 @@
         classNameNode.setId(protoLogImplGenName)
 
         injectConstants(classDeclaration,
-            viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath)
+            viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath, groups,
+            protoLogGroupsClassName)
 
         return code.toString()
     }
@@ -166,7 +179,9 @@
         classDeclaration: ClassOrInterfaceDeclaration,
         viewerConfigFilePath: String,
         legacyViewerConfigFilePath: String?,
-        legacyOutputFilePath: String?
+        legacyOutputFilePath: String?,
+        groups: Map<String, LogGroup>,
+        protoLogGroupsClassName: String
     ) {
         classDeclaration.fields.forEach { field ->
             field.getAnnotationByClass(ProtoLogToolInjected::class.java)
@@ -194,6 +209,35 @@
                                                 StringLiteralExpr(it)
                                             } ?: NullLiteralExpr())
                                 }
+                                ProtoLogToolInjected.Value.LOG_GROUPS.name -> {
+                                    val initializerBlockStmt = BlockStmt()
+                                    for (group in groups) {
+                                        initializerBlockStmt.addStatement(
+                                            MethodCallExpr()
+                                                    .setName("put")
+                                                    .setArguments(
+                                                        NodeList(StringLiteralExpr(group.key),
+                                                            FieldAccessExpr()
+                                                                    .setScope(
+                                                                        NameExpr(
+                                                                            protoLogGroupsClassName
+                                                                        ))
+                                                                    .setName(group.value.name)))
+                                        )
+                                        group.key
+                                    }
+
+                                    val treeMapCreation = ObjectCreationExpr()
+                                            .setType("TreeMap<String, IProtoLogGroup>")
+                                            .setAnonymousClassBody(NodeList(
+                                                InitializerDeclaration().setBody(
+                                                    initializerBlockStmt
+                                                )
+                                            ))
+
+                                    field.setFinal(true)
+                                    field.variables.first().setInitializer(treeMapCreation)
+                                }
                                 else -> error("Unhandled ProtoLogToolInjected value: $valueName.")
                             }
                         }
diff --git a/tools/streaming_proto/Android.bp b/tools/streaming_proto/Android.bp
index b18bdff..b1b314fc 100644
--- a/tools/streaming_proto/Android.bp
+++ b/tools/streaming_proto/Android.bp
@@ -17,6 +17,7 @@
 // ==========================================================
 // Build the host executable: protoc-gen-javastream
 // ==========================================================
+
 package {
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
@@ -41,6 +42,32 @@
     static_libs: ["libprotoc"],
 }
 
+// ==========================================================
+// Build the host static library: java_streaming_proto_lib
+// ==========================================================
+
+cc_library_host_static {
+    name: "java_streaming_proto_lib",
+    defaults: ["protoc-gen-stream-defaults"],
+    target: {
+        darwin: {
+            cflags: ["-D_DARWIN_UNLIMITED_STREAMS"],
+        },
+    },
+    cflags: [
+        "-Wno-format-y2k",
+        "-DSTATIC_ANDROIDFW_FOR_TOOLS",
+    ],
+
+    srcs: [
+        "java/java_proto_stream_code_generator.cpp",
+    ],
+}
+
+// ==========================================================
+// Build the host executable: protoc-gen-javastream
+// ==========================================================
+
 cc_binary_host {
     name: "protoc-gen-javastream",
     srcs: [
@@ -48,8 +75,13 @@
     ],
 
     defaults: ["protoc-gen-stream-defaults"],
+    static_libs: ["java_streaming_proto_lib"],
 }
 
+// ==========================================================
+// Build the host executable: protoc-gen-cppstream
+// ==========================================================
+
 cc_binary_host {
     name: "protoc-gen-cppstream",
     srcs: [
@@ -60,13 +92,31 @@
 }
 
 // ==========================================================
+// Build the host tests: StreamingProtoTest
+// ==========================================================
+
+cc_test_host {
+    name: "StreamingProtoTest",
+    defaults: ["protoc-gen-stream-defaults"],
+    srcs: [
+        "test/unit/**/*.cpp",
+    ],
+    static_libs: [
+        "java_streaming_proto_lib",
+        "libgmock",
+        "libgtest",
+    ],
+}
+
+// ==========================================================
 // Build the java test
 // ==========================================================
+
 java_library {
-    name: "StreamingProtoTest",
+    name: "StreamingProtoJavaIntegrationTest",
     srcs: [
-        "test/**/*.java",
-        "test/**/*.proto",
+        "test/integration/**/*.java",
+        "test/integration/**/*.proto",
     ],
     proto: {
         type: "stream",
diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.cpp b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp
new file mode 100644
index 0000000..9d61111
--- /dev/null
+++ b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "java_proto_stream_code_generator.h"
+
+#include <stdio.h>
+
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <sstream>
+#include <string>
+
+#include "Errors.h"
+
+using namespace android::stream_proto;
+using namespace google::protobuf::io;
+using namespace std;
+
+/**
+ * If the descriptor gives us a class name, use that. Otherwise make one up from
+ * the filename of the .proto file.
+ */
+static string make_outer_class_name(const FileDescriptorProto& file_descriptor) {
+    string name = file_descriptor.options().java_outer_classname();
+    if (name.size() == 0) {
+        name = to_camel_case(file_base_name(file_descriptor.name()));
+        if (name.size() == 0) {
+            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE,
+                       "Unable to make an outer class name for file: %s",
+                       file_descriptor.name().c_str());
+            name = "Unknown";
+        }
+    }
+    return name;
+}
+
+/**
+ * Figure out the package name that we are generating.
+ */
+static string make_java_package(const FileDescriptorProto& file_descriptor) {
+    if (file_descriptor.options().has_java_package()) {
+        return file_descriptor.options().java_package();
+    } else {
+        return file_descriptor.package();
+    }
+}
+
+/**
+ * Figure out the name of the file we are generating.
+ */
+static string make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name) {
+    string const package = make_java_package(file_descriptor);
+    string result;
+    if (package.size() > 0) {
+        result = replace_string(package, '.', '/');
+        result += '/';
+    }
+
+    result += class_name;
+    result += ".java";
+
+    return result;
+}
+
+static string indent_more(const string& indent) {
+    return indent + INDENT;
+}
+
+/**
+ * Write the constants for an enum.
+ */
+static void write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent) {
+    const int N = enu.value_size();
+    text << indent << "// enum " << enu.name() << endl;
+    for (int i = 0; i < N; i++) {
+        const EnumValueDescriptorProto& value = enu.value(i);
+        text << indent << "public static final int " << make_constant_name(value.name()) << " = "
+             << value.number() << ";" << endl;
+    }
+    text << endl;
+}
+
+/**
+ * Write a field.
+ */
+static void write_field(stringstream& text, const FieldDescriptorProto& field,
+                        const string& indent) {
+    string optional_comment =
+            field.label() == FieldDescriptorProto::LABEL_OPTIONAL ? "optional " : "";
+    string repeated_comment =
+            field.label() == FieldDescriptorProto::LABEL_REPEATED ? "repeated " : "";
+    string proto_type = get_proto_type(field);
+    string packed_comment = field.options().packed() ? " [packed=true]" : "";
+    text << indent << "// " << optional_comment << repeated_comment << proto_type << ' '
+         << field.name() << " = " << field.number() << packed_comment << ';' << endl;
+
+    text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x";
+
+    ios::fmtflags fmt(text.flags());
+    text << setfill('0') << setw(16) << hex << get_field_id(field);
+    text.flags(fmt);
+
+    text << "L;" << endl;
+
+    text << endl;
+}
+
+/**
+ * Write a Message constants class.
+ */
+static void write_message(stringstream& text, const DescriptorProto& message,
+                          const string& indent) {
+    int N;
+    const string indented = indent_more(indent);
+
+    text << indent << "// message " << message.name() << endl;
+    text << indent << "public final class " << message.name() << " {" << endl;
+    text << endl;
+
+    // Enums
+    N = message.enum_type_size();
+    for (int i = 0; i < N; i++) {
+        write_enum(text, message.enum_type(i), indented);
+    }
+
+    // Nested classes
+    N = message.nested_type_size();
+    for (int i = 0; i < N; i++) {
+        write_message(text, message.nested_type(i), indented);
+    }
+
+    // Fields
+    N = message.field_size();
+    for (int i = 0; i < N; i++) {
+        write_field(text, message.field(i), indented);
+    }
+
+    text << indent << "}" << endl;
+    text << endl;
+}
+
+/**
+ * Write the contents of a file.
+ *
+ * If there are enums and generate_outer is false, invalid java code will be generated.
+ */
+static void write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor,
+                       const string& filename, bool generate_outer,
+                       const vector<EnumDescriptorProto>& enums,
+                       const vector<DescriptorProto>& messages) {
+    stringstream text;
+
+    string const package_name = make_java_package(file_descriptor);
+    string const outer_class_name = make_outer_class_name(file_descriptor);
+
+    text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl;
+    text << "// source: " << file_descriptor.name() << endl << endl;
+
+    if (package_name.size() > 0) {
+        if (package_name.size() > 0) {
+            text << "package " << package_name << ";" << endl;
+            text << endl;
+        }
+    }
+
+    // This bit of policy is android api rules specific: Raw proto classes
+    // must never be in the API
+    text << "/** @hide */" << endl;
+    //    text << "@android.annotation.TestApi" << endl;
+
+    if (generate_outer) {
+        text << "public final class " << outer_class_name << " {" << endl;
+        text << endl;
+    }
+
+    size_t N;
+    const string indented = generate_outer ? indent_more("") : string();
+
+    N = enums.size();
+    for (size_t i = 0; i < N; i++) {
+        write_enum(text, enums[i], indented);
+    }
+
+    N = messages.size();
+    for (size_t i = 0; i < N; i++) {
+        write_message(text, messages[i], indented);
+    }
+
+    if (generate_outer) {
+        text << "}" << endl;
+    }
+
+    CodeGeneratorResponse::File* file_response = response->add_file();
+    file_response->set_name(filename);
+    file_response->set_content(text.str());
+}
+
+/**
+ * Write one file per class.  Put all of the enums into the "outer" class.
+ */
+static void write_multiple_files(CodeGeneratorResponse* response,
+                                 const FileDescriptorProto& file_descriptor,
+                                 set<string> messages_to_compile) {
+    // If there is anything to put in the outer class file, create one
+    if (file_descriptor.enum_type_size() > 0) {
+        vector<EnumDescriptorProto> enums;
+        int N = file_descriptor.enum_type_size();
+        for (int i = 0; i < N; i++) {
+            auto enum_full_name =
+                    file_descriptor.package() + "." + file_descriptor.enum_type(i).name();
+            if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) {
+                continue;
+            }
+            enums.push_back(file_descriptor.enum_type(i));
+        }
+
+        vector<DescriptorProto> messages;
+
+        if (messages_to_compile.empty() || !enums.empty()) {
+            write_file(response, file_descriptor,
+                       make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
+                       true, enums, messages);
+        }
+    }
+
+    // For each of the message types, make a file
+    int N = file_descriptor.message_type_size();
+    for (int i = 0; i < N; i++) {
+        vector<EnumDescriptorProto> enums;
+
+        vector<DescriptorProto> messages;
+
+        auto message_full_name =
+                file_descriptor.package() + "." + file_descriptor.message_type(i).name();
+        if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) {
+            continue;
+        }
+        messages.push_back(file_descriptor.message_type(i));
+
+        if (messages_to_compile.empty() || !messages.empty()) {
+            write_file(response, file_descriptor,
+                       make_file_name(file_descriptor, file_descriptor.message_type(i).name()),
+                       false, enums, messages);
+        }
+    }
+}
+
+static void write_single_file(CodeGeneratorResponse* response,
+                              const FileDescriptorProto& file_descriptor,
+                              set<string> messages_to_compile) {
+    int N;
+
+    vector<EnumDescriptorProto> enums;
+    N = file_descriptor.enum_type_size();
+    for (int i = 0; i < N; i++) {
+        auto enum_full_name = file_descriptor.package() + "." + file_descriptor.enum_type(i).name();
+        if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) {
+            continue;
+        }
+
+        enums.push_back(file_descriptor.enum_type(i));
+    }
+
+    vector<DescriptorProto> messages;
+    N = file_descriptor.message_type_size();
+    for (int i = 0; i < N; i++) {
+        auto message_full_name =
+                file_descriptor.package() + "." + file_descriptor.message_type(i).name();
+
+        if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) {
+            continue;
+        }
+
+        messages.push_back(file_descriptor.message_type(i));
+    }
+
+    if (messages_to_compile.empty() || !enums.empty() || !messages.empty()) {
+        write_file(response, file_descriptor,
+                   make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), true,
+                   enums, messages);
+    }
+}
+
+static void parse_args_string(stringstream args_string_stream,
+                              set<string>* messages_to_compile_out) {
+    string line;
+    while (getline(args_string_stream, line, ';')) {
+        stringstream line_ss(line);
+        string arg_name;
+        getline(line_ss, arg_name, ':');
+        if (arg_name == "include_filter") {
+            string full_message_name;
+            while (getline(line_ss, full_message_name, ',')) {
+                messages_to_compile_out->insert(full_message_name);
+            }
+        } else {
+            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE, "Unexpected argument '%s'.", arg_name.c_str());
+        }
+    }
+}
+
+CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request) {
+    CodeGeneratorResponse response;
+
+    set<string> messages_to_compile;
+    auto request_params = request.parameter();
+    if (!request_params.empty()) {
+        parse_args_string(stringstream(request_params), &messages_to_compile);
+    }
+
+    // Build the files we need.
+    const int N = request.proto_file_size();
+    for (int i = 0; i < N; i++) {
+        const FileDescriptorProto& file_descriptor = request.proto_file(i);
+        if (should_generate_for_file(request, file_descriptor.name())) {
+            if (file_descriptor.options().java_multiple_files()) {
+                write_multiple_files(&response, file_descriptor, messages_to_compile);
+            } else {
+                write_single_file(&response, file_descriptor, messages_to_compile);
+            }
+        }
+    }
+
+    return response;
+}
diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.h b/tools/streaming_proto/java/java_proto_stream_code_generator.h
new file mode 100644
index 0000000..d2492f7
--- /dev/null
+++ b/tools/streaming_proto/java/java_proto_stream_code_generator.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
+#define AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
+
+#include "stream_proto_utils.h"
+#include "string_utils.h"
+
+using namespace android::stream_proto;
+using namespace google::protobuf::io;
+using namespace std;
+
+CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request);
+
+#endif // AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
\ No newline at end of file
diff --git a/tools/streaming_proto/java/main.cpp b/tools/streaming_proto/java/main.cpp
index c9c50a5..5b35504 100644
--- a/tools/streaming_proto/java/main.cpp
+++ b/tools/streaming_proto/java/main.cpp
@@ -1,268 +1,21 @@
-#include "Errors.h"
-#include "stream_proto_utils.h"
-#include "string_utils.h"
-
 #include <stdio.h>
+
 #include <iomanip>
 #include <iostream>
-#include <sstream>
 #include <map>
+#include <sstream>
+#include <string>
+
+#include "Errors.h"
+#include "java_proto_stream_code_generator.h"
+#include "stream_proto_utils.h"
 
 using namespace android::stream_proto;
 using namespace google::protobuf::io;
 using namespace std;
 
 /**
- * If the descriptor gives us a class name, use that. Otherwise make one up from
- * the filename of the .proto file.
- */
-static string
-make_outer_class_name(const FileDescriptorProto& file_descriptor)
-{
-    string name = file_descriptor.options().java_outer_classname();
-    if (name.size() == 0) {
-        name = to_camel_case(file_base_name(file_descriptor.name()));
-        if (name.size() == 0) {
-            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE,
-                    "Unable to make an outer class name for file: %s",
-                    file_descriptor.name().c_str());
-            name = "Unknown";
-        }
-    }
-    return name;
-}
-
-/**
- * Figure out the package name that we are generating.
- */
-static string
-make_java_package(const FileDescriptorProto& file_descriptor) {
-    if (file_descriptor.options().has_java_package()) {
-        return file_descriptor.options().java_package();
-    } else {
-        return file_descriptor.package();
-    }
-}
-
-/**
- * Figure out the name of the file we are generating.
- */
-static string
-make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name)
-{
-    string const package = make_java_package(file_descriptor);
-    string result;
-    if (package.size() > 0) {
-        result = replace_string(package, '.', '/');
-        result += '/';
-    }
-
-    result += class_name;
-    result += ".java";
-
-    return result;
-}
-
-static string
-indent_more(const string& indent)
-{
-    return indent + INDENT;
-}
-
-/**
- * Write the constants for an enum.
- */
-static void
-write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent)
-{
-    const int N = enu.value_size();
-    text << indent << "// enum " << enu.name() << endl;
-    for (int i=0; i<N; i++) {
-        const EnumValueDescriptorProto& value = enu.value(i);
-        text << indent << "public static final int "
-                << make_constant_name(value.name())
-                << " = " << value.number() << ";" << endl;
-    }
-    text << endl;
-}
-
-/**
- * Write a field.
- */
-static void
-write_field(stringstream& text, const FieldDescriptorProto& field, const string& indent)
-{
-    string optional_comment = field.label() == FieldDescriptorProto::LABEL_OPTIONAL
-            ? "optional " : "";
-    string repeated_comment = field.label() == FieldDescriptorProto::LABEL_REPEATED
-            ? "repeated " : "";
-    string proto_type = get_proto_type(field);
-    string packed_comment = field.options().packed()
-            ? " [packed=true]" : "";
-    text << indent << "// " << optional_comment << repeated_comment << proto_type << ' '
-            << field.name() << " = " << field.number() << packed_comment << ';' << endl;
-
-    text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x";
-
-    ios::fmtflags fmt(text.flags());
-    text << setfill('0') << setw(16) << hex << get_field_id(field);
-    text.flags(fmt);
-
-    text << "L;" << endl;
-
-    text << endl;
-}
-
-/**
- * Write a Message constants class.
- */
-static void
-write_message(stringstream& text, const DescriptorProto& message, const string& indent)
-{
-    int N;
-    const string indented = indent_more(indent);
-
-    text << indent << "// message " << message.name() << endl;
-    text << indent << "public final class " << message.name() << " {" << endl;
-    text << endl;
-
-    // Enums
-    N = message.enum_type_size();
-    for (int i=0; i<N; i++) {
-        write_enum(text, message.enum_type(i), indented);
-    }
-
-    // Nested classes
-    N = message.nested_type_size();
-    for (int i=0; i<N; i++) {
-        write_message(text, message.nested_type(i), indented);
-    }
-
-    // Fields
-    N = message.field_size();
-    for (int i=0; i<N; i++) {
-        write_field(text, message.field(i), indented);
-    }
-
-    text << indent << "}" << endl;
-    text << endl;
-}
-
-/**
- * Write the contents of a file.
  *
- * If there are enums and generate_outer is false, invalid java code will be generated.
- */
-static void
-write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor,
-        const string& filename, bool generate_outer,
-        const vector<EnumDescriptorProto>& enums, const vector<DescriptorProto>& messages)
-{
-    stringstream text;
-
-    string const package_name = make_java_package(file_descriptor);
-    string const outer_class_name = make_outer_class_name(file_descriptor);
-
-    text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl;
-    text << "// source: " << file_descriptor.name() << endl << endl;
-
-    if (package_name.size() > 0) {
-        if (package_name.size() > 0) {
-            text << "package " << package_name << ";" << endl;
-            text << endl;
-        }
-    }
-
-    // This bit of policy is android api rules specific: Raw proto classes
-    // must never be in the API
-    text << "/** @hide */" << endl;
-//    text << "@android.annotation.TestApi" << endl;
-
-    if (generate_outer) {
-        text << "public final class " << outer_class_name << " {" << endl;
-        text << endl;
-    }
-
-    size_t N;
-    const string indented = generate_outer ? indent_more("") : string();
-    
-    N = enums.size();
-    for (size_t i=0; i<N; i++) {
-        write_enum(text, enums[i], indented);
-    }
-
-    N = messages.size();
-    for (size_t i=0; i<N; i++) {
-        write_message(text, messages[i], indented);
-    }
-
-    if (generate_outer) {
-        text << "}" << endl;
-    }
-
-    CodeGeneratorResponse::File* file_response = response->add_file();
-    file_response->set_name(filename);
-    file_response->set_content(text.str());
-}
-
-/**
- * Write one file per class.  Put all of the enums into the "outer" class.
- */
-static void
-write_multiple_files(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor)
-{
-    // If there is anything to put in the outer class file, create one
-    if (file_descriptor.enum_type_size() > 0) {
-        vector<EnumDescriptorProto> enums;
-        int N = file_descriptor.enum_type_size();
-        for (int i=0; i<N; i++) {
-            enums.push_back(file_descriptor.enum_type(i));
-        }
-
-        vector<DescriptorProto> messages;
-
-        write_file(response, file_descriptor,
-                make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
-                true, enums, messages);
-    }
-
-    // For each of the message types, make a file
-    int N = file_descriptor.message_type_size();
-    for (int i=0; i<N; i++) {
-        vector<EnumDescriptorProto> enums;
-
-        vector<DescriptorProto> messages;
-        messages.push_back(file_descriptor.message_type(i));
-
-        write_file(response, file_descriptor,
-                make_file_name(file_descriptor, file_descriptor.message_type(i).name()),
-                false, enums, messages);
-    }
-}
-
-static void
-write_single_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor)
-{
-    int N;
-
-    vector<EnumDescriptorProto> enums;
-    N = file_descriptor.enum_type_size();
-    for (int i=0; i<N; i++) {
-        enums.push_back(file_descriptor.enum_type(i));
-    }
-
-    vector<DescriptorProto> messages;
-    N = file_descriptor.message_type_size();
-    for (int i=0; i<N; i++) {
-        messages.push_back(file_descriptor.message_type(i));
-    }
-
-    write_file(response, file_descriptor,
-            make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
-            true, enums, messages);
-}
-
-/**
  * Main.
  */
 int
@@ -273,24 +26,11 @@
 
     GOOGLE_PROTOBUF_VERIFY_VERSION;
 
-    CodeGeneratorRequest request;
-    CodeGeneratorResponse response;
-
     // Read the request
+    CodeGeneratorRequest request;
     request.ParseFromIstream(&cin);
 
-    // Build the files we need.
-    const int N = request.proto_file_size();
-    for (int i=0; i<N; i++) {
-        const FileDescriptorProto& file_descriptor = request.proto_file(i);
-        if (should_generate_for_file(request, file_descriptor.name())) {
-            if (file_descriptor.options().java_multiple_files()) {
-                write_multiple_files(&response, file_descriptor);
-            } else {
-                write_single_file(&response, file_descriptor);
-            }
-        }
-    }
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
 
     // If we had errors, don't write the response. Print the errors and exit.
     if (ERRORS.HasErrors()) {
diff --git a/tools/streaming_proto/test/imported.proto b/tools/streaming_proto/test/integration/imported.proto
similarity index 100%
rename from tools/streaming_proto/test/imported.proto
rename to tools/streaming_proto/test/integration/imported.proto
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
similarity index 66%
copy from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
copy to tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
index 838e41e..2a7001b 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2024, The Android Open Source Project
+/*
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package com.android.streaming_proto_test;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
+public class Main {
+    public void main(String[] argv) {
+        System.out.println("hello world");
+    }
 }
-
diff --git a/tools/streaming_proto/test/test.proto b/tools/streaming_proto/test/integration/test.proto
similarity index 97%
rename from tools/streaming_proto/test/test.proto
rename to tools/streaming_proto/test/integration/test.proto
index de80ed6..3cf81b4 100644
--- a/tools/streaming_proto/test/test.proto
+++ b/tools/streaming_proto/test/integration/test.proto
@@ -16,7 +16,7 @@
 
 syntax = "proto2";
 
-import "frameworks/base/tools/streaming_proto/test/imported.proto";
+import "frameworks/base/tools/streaming_proto/test/integration/imported.proto";
 
 package com.android.streaming_proto_test;
 
diff --git a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java b/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java
deleted file mode 100644
index 1246f53..0000000
--- a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.android.streaming_proto_test;
-
-public class Main {
-    public void main(String[] argv) {
-        System.out.println("hello world");
-    }
-}
diff --git a/tools/streaming_proto/test/unit/streaming_proto_java.cpp b/tools/streaming_proto/test/unit/streaming_proto_java.cpp
new file mode 100644
index 0000000..8df9716
--- /dev/null
+++ b/tools/streaming_proto/test/unit/streaming_proto_java.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "java/java_proto_stream_code_generator.h"
+
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+static void add_my_test_proto_file(CodeGeneratorRequest* request) {
+    request->add_file_to_generate("MyTestProtoFile");
+
+    FileDescriptorProto* file_desc = request->add_proto_file();
+    file_desc->set_name("MyTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(false);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_message");
+}
+
+static void add_my_other_test_proto_file(CodeGeneratorRequest* request) {
+    request->add_file_to_generate("MyOtherTestProtoFile");
+
+    FileDescriptorProto* file_desc = request->add_proto_file();
+    file_desc->set_name("MyOtherTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(false);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyOtherTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("a_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("another_test_field");
+}
+
+static CodeGeneratorRequest create_simple_two_file_request() {
+    CodeGeneratorRequest request;
+
+    add_my_test_proto_file(&request);
+    add_my_other_test_proto_file(&request);
+
+    return request;
+}
+
+static CodeGeneratorRequest create_simple_multi_file_request() {
+    CodeGeneratorRequest request;
+
+    request.add_file_to_generate("MyMultiMessageTestProtoFile");
+
+    FileDescriptorProto* file_desc = request.add_proto_file();
+    file_desc->set_name("MyMultiMessageTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(true);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_message");
+
+    message = file_desc->add_message_type();
+    message->set_name("MyOtherTestMessage");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("a_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("another_test_field");
+
+    return request;
+}
+
+TEST(StreamingProtoJavaTest, NoFilter) {
+    CodeGeneratorRequest request = create_simple_two_file_request();
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 2);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java");
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+
+    EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestProtoFile.java");
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestProtoFile"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithFilter) {
+    CodeGeneratorRequest request = create_simple_two_file_request();
+    request.set_parameter("include_filter:test.package.MyTestMessage");
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 1);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java");
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithoutFilter_MultipleJavaFiles) {
+    CodeGeneratorRequest request = create_simple_multi_file_request();
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 2);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java");
+    EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile")));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+
+    EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestMessage.java");
+    EXPECT_THAT(response.file(1).content(), Not(HasSubstr("class MyOtherTestProtoFile")));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithFilter_MultipleJavaFiles) {
+    CodeGeneratorRequest request = create_simple_multi_file_request();
+    request.set_parameter("include_filter:test.package.MyTestMessage");
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 1);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java");
+    EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile")));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+}
diff --git a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
index 58638e8..45ab986 100644
--- a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
+++ b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
@@ -718,6 +718,9 @@
         } catch (RemoteException e1) {
             Log.e(TAG, "Failed to get IClientInterface due to remote exception");
             return false;
+        } catch (NullPointerException e2) {
+            Log.e(TAG, "setupInterfaceForClientMode NullPointerException");
+            return false;
         }
 
         if (clientInterface == null) {
@@ -785,6 +788,9 @@
         } catch (RemoteException e1) {
             Log.e(TAG, "Failed to teardown client interface due to remote exception");
             return false;
+        } catch (NullPointerException e2) {
+            Log.e(TAG, "tearDownClientInterface NullPointerException");
+            return false;
         }
         if (!success) {
             Log.e(TAG, "Failed to teardown client interface");
@@ -816,6 +822,9 @@
         } catch (RemoteException e1) {
             Log.e(TAG, "Failed to get IApInterface due to remote exception");
             return false;
+        } catch (NullPointerException e2) {
+            Log.e(TAG, "setupInterfaceForSoftApMode NullPointerException");
+            return false;
         }
 
         if (apInterface == null) {
@@ -854,6 +863,9 @@
         } catch (RemoteException e1) {
             Log.e(TAG, "Failed to teardown AP interface due to remote exception");
             return false;
+        } catch (NullPointerException e2) {
+            Log.e(TAG, "tearDownSoftApInterface NullPointerException");
+            return false;
         }
         if (!success) {
             Log.e(TAG, "Failed to teardown AP interface");
@@ -1328,6 +1340,8 @@
             }
         } catch (RemoteException e1) {
             Log.e(TAG, "Failed to request getChannelsForBand due to remote exception");
+        } catch (NullPointerException e2) {
+            Log.e(TAG, "getChannelsMhzForBand NullPointerException");
         }
         if (result == null) {
             result = new int[0];
@@ -1352,7 +1366,8 @@
      */
     @Nullable public DeviceWiphyCapabilities getDeviceWiphyCapabilities(@NonNull String ifaceName) {
         if (mWificond == null) {
-            Log.e(TAG, "getDeviceWiphyCapabilities: mWificond binder is null! Did wificond die?");
+            Log.e(TAG, "getDeviceWiphyCapabilities: mWificond binder is null! "
+                    + "Did wificond die?");
             return null;
         }
 
@@ -1360,6 +1375,9 @@
             return mWificond.getDeviceWiphyCapabilities(ifaceName);
         } catch (RemoteException e) {
             return null;
+        } catch (NullPointerException e2) {
+            Log.e(TAG, "getDeviceWiphyCapabilities NullPointerException");
+            return null;
         }
     }
 
@@ -1409,6 +1427,8 @@
             Log.i(TAG, "Receive country code change to " + newCountryCode);
         } catch (RemoteException re) {
             re.rethrowFromSystemServer();
+        } catch (NullPointerException e) {
+            new RemoteException("Wificond service doesn't exist!").rethrowFromSystemServer();
         }
     }