Merge "Remove usages of TimestampedValue<Long>"
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f221eca..134b71a4 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -167,6 +167,7 @@
     field public static final String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS";
     field public static final String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING";
     field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS";
+    field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS";
     field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS";
     field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
     field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY";
@@ -2789,6 +2790,8 @@
 
   public final class VirtualDeviceManager {
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams);
+    field public static final int DEFAULT_DEVICE_ID = 0; // 0x0
+    field public static final int INVALID_DEVICE_ID = -1; // 0xffffffff
     field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2
     field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1
     field public static final int LAUNCH_SUCCESS = 0; // 0x0
@@ -2808,6 +2811,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method public int getDeviceId();
     method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index b4abd3c..6404a1f 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -524,9 +524,30 @@
 
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     void suppressResizeConfigChanges(boolean suppress);
+
+    /**
+     * @deprecated Use {@link #unlockUser2(int, IProgressListener)} instead, since the token and
+     * secret arguments no longer do anything.  This method still exists only because it is marked
+     * with {@code @UnsupportedAppUsage}, so it might not be safe to remove it or change its
+     * signature.
+     */
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     boolean unlockUser(int userid, in byte[] token, in byte[] secret,
             in IProgressListener listener);
+
+    /**
+     * Tries to unlock the given user.
+     * <p>
+     * This will succeed only if the user's CE storage key is already unlocked or if the user
+     * doesn't have a lockscreen credential set.
+     *
+     * @param userId The ID of the user to unlock.
+     * @param listener An optional progress listener.
+     *
+     * @return true if the user was successfully unlocked, otherwise false.
+     */
+    boolean unlockUser2(int userId, in IProgressListener listener);
+
     void killPackageDependents(in String packageName, int userId);
     void makePackageIdle(String packageName, int userId);
     int getMemoryTrimLevel();
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index d5b85cd..4ddfdb6 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -81,6 +81,8 @@
 import android.content.pm.verify.domain.IDomainVerificationManager;
 import android.content.res.Resources;
 import android.content.rollback.RollbackManagerFrameworkInitializer;
+import android.credentials.CredentialManager;
+import android.credentials.ICredentialManager;
 import android.debug.AdbManager;
 import android.debug.IAdbManager;
 import android.graphics.fonts.FontManager;
@@ -1138,6 +1140,19 @@
                 return new AutofillManager(ctx.getOuterContext(), service);
             }});
 
+        registerService(Context.CREDENTIAL_SERVICE, CredentialManager.class,
+                new CachedServiceFetcher<CredentialManager>() {
+                    @Override
+                    public CredentialManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder b = ServiceManager.getService(Context.CREDENTIAL_SERVICE);
+                        ICredentialManager service = ICredentialManager.Stub.asInterface(b);
+                        if (service != null) {
+                            return new CredentialManager(ctx.getOuterContext(), service);
+                        }
+                        return null;
+                    }});
+
         registerService(Context.MUSIC_RECOGNITION_SERVICE, MusicRecognitionManager.class,
                 new CachedServiceFetcher<MusicRecognitionManager>() {
                     @Override
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 9c99da5..e7f19166 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -43,6 +43,11 @@
     int getAssociationId();
 
     /**
+     * Returns the unique device ID for this virtual device.
+     */
+    int getDeviceId();
+
+    /**
      * Closes the virtual device and frees all associated resources.
      */
     void close();
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index fadfa5c..08bee25 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -78,6 +78,16 @@
                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 
+    /**
+     * The default device ID, which is the ID of the primary (non-virtual) device.
+     */
+    public static final int DEFAULT_DEVICE_ID = 0;
+
+    /**
+     * Invalid device ID.
+     */
+    public static final int INVALID_DEVICE_ID = -1;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
@@ -204,6 +214,17 @@
         }
 
         /**
+         * Returns the unique ID of this virtual device.
+         */
+        public int getDeviceId() {
+            try {
+                return mVirtualDevice.getDeviceId();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Launches a given pending intent on the give display ID.
          *
          * @param displayId The display to launch the pending intent on. This display must be
diff --git a/core/java/android/credentials/CreateCredentialRequest.java b/core/java/android/credentials/CreateCredentialRequest.java
index 333c837..22ef230 100644
--- a/core/java/android/credentials/CreateCredentialRequest.java
+++ b/core/java/android/credentials/CreateCredentialRequest.java
@@ -45,6 +45,11 @@
     private final Bundle mData;
 
     /**
+     * Determines whether or not the request must only be fulfilled by a system provider.
+     */
+    private final boolean mRequireSystemProvider;
+
+    /**
      * Returns the requested credential type.
      */
     @NonNull
@@ -60,10 +65,21 @@
         return mData;
     }
 
+    /**
+     * Returns true if the request must only be fulfilled by a system provider, and false
+     * otherwise.
+     *
+     * @hide
+     */
+    public boolean requireSystemProvider() {
+        return mRequireSystemProvider;
+    }
+
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mType);
         dest.writeBundle(mData);
+        dest.writeBoolean(mRequireSystemProvider);
     }
 
     @Override
@@ -73,30 +89,56 @@
 
     @Override
     public String toString() {
-        return "CreateCredentialRequest {" + "type=" + mType + ", data=" + mData + "}";
+        return "CreateCredentialRequest {"
+                + "type=" + mType
+                + ", data=" + mData
+                + ", requireSystemProvider=" + mRequireSystemProvider
+                + "}";
     }
 
     /**
      * Constructs a {@link CreateCredentialRequest}.
      *
-     * @param type the requested credential type.
-     * @param data the request data.
+     * @param type the requested credential type
+     * @param data the request data
      *
-     * @throws IllegalArgumentException If type is empty.
+     * @throws IllegalArgumentException If type is empty
      */
     public CreateCredentialRequest(@NonNull String type, @NonNull Bundle data) {
+        this(type, data, /*requireSystemProvider=*/ false);
+    }
+
+    /**
+     * Constructs a {@link CreateCredentialRequest}.
+     *
+     * @param type the requested credential type
+     * @param data the request data
+     * @param requireSystemProvider whether or not the request must only be fulfilled by a system
+     *                              provider
+     *
+     * @throws IllegalArgumentException If type is empty.
+     *
+     * @hide
+     */
+    public CreateCredentialRequest(
+            @NonNull String type,
+            @NonNull Bundle data,
+            boolean requireSystemProvider) {
         mType = Preconditions.checkStringNotEmpty(type, "type must not be empty");
         mData = requireNonNull(data, "data must not be null");
+        mRequireSystemProvider = requireSystemProvider;
     }
 
     private CreateCredentialRequest(@NonNull Parcel in) {
         String type = in.readString8();
         Bundle data = in.readBundle();
+        boolean requireSystemProvider = in.readBoolean();
 
         mType = type;
         AnnotationValidations.validate(NonNull.class, null, mType);
         mData = data;
         AnnotationValidations.validate(NonNull.class, null, mData);
+        mRequireSystemProvider = requireSystemProvider;
     }
 
     public static final @NonNull Parcelable.Creator<CreateCredentialRequest> CREATOR =
diff --git a/core/java/android/credentials/CredentialManagerException.java b/core/java/android/credentials/CredentialManagerException.java
index 116395c..8369649 100644
--- a/core/java/android/credentials/CredentialManagerException.java
+++ b/core/java/android/credentials/CredentialManagerException.java
@@ -23,6 +23,20 @@
     /** Indicates that an unknown error was encountered. */
     public static final int ERROR_UNKNOWN = 0;
 
+    /**
+     * The given CredentialManager operation is cancelled by the user.
+     *
+     * @hide
+     */
+    public static final int ERROR_USER_CANCELLED = 1;
+
+    /**
+     * No appropriate provider is found to support the target credential type(s).
+     *
+     * @hide
+     */
+    public static final int ERROR_PROVIDER_NOT_FOUND = 2;
+
     public final int errorCode;
 
     public CredentialManagerException(int errorCode, @Nullable String message) {
diff --git a/core/java/android/credentials/GetCredentialOption.java b/core/java/android/credentials/GetCredentialOption.java
index af58b08..a0d3c0b 100644
--- a/core/java/android/credentials/GetCredentialOption.java
+++ b/core/java/android/credentials/GetCredentialOption.java
@@ -44,6 +44,11 @@
     private final Bundle mData;
 
     /**
+     * Determines whether or not the request must only be fulfilled by a system provider.
+     */
+    private final boolean mRequireSystemProvider;
+
+    /**
      * Returns the requested credential type.
      */
     @NonNull
@@ -59,10 +64,21 @@
         return mData;
     }
 
+    /**
+     * Returns true if the request must only be fulfilled by a system provider, and false
+     * otherwise.
+     *
+     * @hide
+     */
+    public boolean requireSystemProvider() {
+        return mRequireSystemProvider;
+    }
+
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mType);
         dest.writeBundle(mData);
+        dest.writeBoolean(mRequireSystemProvider);
     }
 
     @Override
@@ -72,30 +88,56 @@
 
     @Override
     public String toString() {
-        return "GetCredentialOption {" + "type=" + mType + ", data=" + mData + "}";
+        return "GetCredentialOption {"
+                + "type=" + mType
+                + ", data=" + mData
+                + ", requireSystemProvider=" + mRequireSystemProvider
+                + "}";
     }
 
     /**
      * Constructs a {@link GetCredentialOption}.
      *
-     * @param type the requested credential type.
-     * @param data the request data.
+     * @param type the requested credential type
+     * @param data the request data
      *
-     * @throws IllegalArgumentException If type is empty.
+     * @throws IllegalArgumentException If type is empty
      */
     public GetCredentialOption(@NonNull String type, @NonNull Bundle data) {
+        this(type, data, /*requireSystemProvider=*/ false);
+    }
+
+    /**
+     * Constructs a {@link GetCredentialOption}.
+     *
+     * @param type the requested credential type
+     * @param data the request data
+     * @param requireSystemProvider whether or not the request must only be fulfilled by a system
+     *                              provider
+     *
+     * @throws IllegalArgumentException If type is empty.
+     *
+     * @hide
+     */
+    public GetCredentialOption(
+            @NonNull String type,
+            @NonNull Bundle data,
+            boolean requireSystemProvider) {
         mType = Preconditions.checkStringNotEmpty(type, "type must not be empty");
         mData = requireNonNull(data, "data must not be null");
+        mRequireSystemProvider = requireSystemProvider;
     }
 
     private GetCredentialOption(@NonNull Parcel in) {
         String type = in.readString8();
         Bundle data = in.readBundle();
+        boolean requireSystemProvider = in.readBoolean();
 
         mType = type;
         AnnotationValidations.validate(NonNull.class, null, mType);
         mData = data;
         AnnotationValidations.validate(NonNull.class, null, mData);
+        mRequireSystemProvider = requireSystemProvider;
     }
 
     public static final @NonNull Parcelable.Creator<GetCredentialOption> CREATOR =
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index c0e2864..9bc7ffd 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -2913,6 +2913,7 @@
         private int mFlags;
         @UnsupportedAppUsage
         private String mTag;
+        private int mTagHash;
         private final String mPackageName;
         private final IBinder mToken;
         private int mInternalCount;
@@ -2921,7 +2922,6 @@
         private boolean mHeld;
         private WorkSource mWorkSource;
         private String mHistoryTag;
-        private final String mTraceName;
         private final int mDisplayId;
         private WakeLockStateListener mListener;
         private IWakeLockCallback mCallback;
@@ -2931,9 +2931,9 @@
         WakeLock(int flags, String tag, String packageName, int displayId) {
             mFlags = flags;
             mTag = tag;
+            mTagHash = mTag.hashCode();
             mPackageName = packageName;
             mToken = new Binder();
-            mTraceName = "WakeLock (" + mTag + ")";
             mDisplayId = displayId;
         }
 
@@ -2942,7 +2942,8 @@
             synchronized (mToken) {
                 if (mHeld) {
                     Log.wtf(TAG, "WakeLock finalized while still held: " + mTag);
-                    Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+                    Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+                            "WakeLocks", mTagHash);
                     try {
                         mService.releaseWakeLock(mToken, 0);
                     } catch (RemoteException e) {
@@ -3012,7 +3013,8 @@
                 // should immediately acquire the wake lock once again despite never having
                 // been explicitly released by the keyguard.
                 mHandler.removeCallbacks(mReleaser);
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0);
+                Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER,
+                        "WakeLocks", mTag, mTagHash);
                 try {
                     mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource,
                             mHistoryTag, mDisplayId, mCallback);
@@ -3060,7 +3062,8 @@
                 if (!mRefCounted || mInternalCount == 0) {
                     mHandler.removeCallbacks(mReleaser);
                     if (mHeld) {
-                        Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+                        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+                                "WakeLocks", mTagHash);
                         try {
                             mService.releaseWakeLock(mToken, flags);
                         } catch (RemoteException e) {
@@ -3137,6 +3140,7 @@
         /** @hide */
         public void setTag(String tag) {
             mTag = tag;
+            mTagHash = mTag.hashCode();
         }
 
         /** @hide */
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index c943a3d..e483328 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -1482,6 +1482,18 @@
     public static final native int killProcessGroup(int uid, int pid);
 
     /**
+      * Freeze the cgroup for the given UID.
+      * This cgroup may contain child cgroups which will also be frozen. If this cgroup or its
+      * children contain processes with Binder interfaces, those interfaces should be frozen before
+      * the cgroup to avoid blocking synchronous callers indefinitely.
+      *
+      * @param uid The UID to be frozen
+      * @param freeze true = freeze; false = unfreeze
+      * @hide
+      */
+    public static final native void freezeCgroupUid(int uid, boolean freeze);
+
+    /**
      * Remove all process groups.  Expected to be called when ActivityManager
      * is restarted.
      * @hide
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index df0bee7..bc52744 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -137,6 +137,7 @@
     void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61;
     @EnforcePermission("STORAGE_INTERNAL")
     void destroyUserKey(int userId) = 62;
+    @EnforcePermission("STORAGE_INTERNAL")
     void unlockUserKey(int userId, int serialNumber, in byte[] secret) = 63;
     @EnforcePermission("STORAGE_INTERNAL")
     void lockUserKey(int userId) = 64;
@@ -146,9 +147,7 @@
     @EnforcePermission("STORAGE_INTERNAL")
     void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67;
     @EnforcePermission("STORAGE_INTERNAL")
-    void addUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 70;
-    @EnforcePermission("STORAGE_INTERNAL")
-    void fixateNewestUserKeyAuth(int userId) = 71;
+    void setUserKeyProtection(int userId, in byte[] secret) = 70;
     @EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
     void fstrim(int flags, IVoldTaskListener listener) = 72;
     AppFuseMount mountProxyFileDescriptorBridge() = 73;
@@ -165,8 +164,6 @@
     @EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
     boolean needsCheckpoint() = 86;
     void abortChanges(in String message, boolean retry) = 87;
-    @EnforcePermission("STORAGE_INTERNAL")
-    void clearUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 88;
     void fixupAppDir(in String path) = 89;
     void disableAppDataIsolation(in String pkgName, int pid, int userId) = 90;
     PendingIntent getManageSpaceActivityIntent(in String packageName, int requestCode) = 91;
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index c1606e8..38ac984 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1606,15 +1606,6 @@
     }
 
     /** {@hide} */
-    public void unlockUserKey(int userId, int serialNumber, byte[] secret) {
-        try {
-            mStorageManager.unlockUserKey(userId, serialNumber, secret);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /** {@hide} */
     public void lockUserKey(int userId) {
         try {
             mStorageManager.lockUserKey(userId);
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 99b9156..7095d1b 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -377,6 +377,14 @@
     public static final String NAMESPACE_REBOOT_READINESS = "reboot_readiness";
 
     /**
+     * Namespace for Remote Key Provisioning related features.
+     *
+     * @hide
+     */
+    public static final String NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE =
+            "remote_key_provisioning_native";
+
+    /**
      * Namespace for Rollback flags that are applied immediately.
      *
      * @hide
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cab6acb..00633a2 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -393,6 +393,21 @@
             "android.settings.ACCESSIBILITY_DETAILS_SETTINGS";
 
     /**
+     * Activity Action: Show settings to allow configuration of accessibility color and motion.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_ACCESSIBILITY_COLOR_MOTION_SETTINGS =
+            "android.settings.ACCESSIBILITY_COLOR_MOTION_SETTINGS";
+
+    /**
      * Activity Action: Show settings to allow configuration of Reduce Bright Colors.
      * <p>
      * In some cases, a matching Activity may not exist, so ensure you
@@ -438,6 +453,21 @@
             "android.settings.COLOR_INVERSION_SETTINGS";
 
     /**
+     * Activity Action: Show settings to allow configuration of text reading.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_TEXT_READING_SETTINGS =
+            "android.settings.TEXT_READING_SETTINGS";
+
+    /**
      * Activity Action: Show settings to control access to usage information.
      * <p>
      * In some cases, a matching Activity may not exist, so ensure you
diff --git a/core/java/android/security/keymaster/KeymasterDefs.java b/core/java/android/security/keymaster/KeymasterDefs.java
index 8efc5eb..e720f1a 100644
--- a/core/java/android/security/keymaster/KeymasterDefs.java
+++ b/core/java/android/security/keymaster/KeymasterDefs.java
@@ -65,6 +65,7 @@
     public static final int KM_TAG_PADDING = Tag.PADDING; // KM_ENUM_REP | 6;
     public static final int KM_TAG_CALLER_NONCE = Tag.CALLER_NONCE; // KM_BOOL | 7;
     public static final int KM_TAG_MIN_MAC_LENGTH = Tag.MIN_MAC_LENGTH; // KM_UINT | 8;
+    public static final int KM_TAG_EC_CURVE = Tag.EC_CURVE; // KM_ENUM | 10;
 
     public static final int KM_TAG_RSA_PUBLIC_EXPONENT = Tag.RSA_PUBLIC_EXPONENT; // KM_ULONG | 200;
     public static final int KM_TAG_INCLUDE_UNIQUE_ID = Tag.INCLUDE_UNIQUE_ID; // KM_BOOL | 202;
diff --git a/core/java/android/service/dreams/Sandman.java b/core/java/android/service/dreams/Sandman.java
index fae72a2..ced2a01 100644
--- a/core/java/android/service/dreams/Sandman.java
+++ b/core/java/android/service/dreams/Sandman.java
@@ -20,13 +20,13 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.Slog;
 
-import com.android.server.LocalServices;
-
 /**
  * Internal helper for launching dreams to ensure consistency between the
  * <code>UiModeManagerService</code> system service and the <code>Somnambulator</code> activity.
@@ -75,28 +75,32 @@
     }
 
     private static void startDream(Context context, boolean docked) {
-        DreamManagerInternal dreamManagerService =
-                LocalServices.getService(DreamManagerInternal.class);
-        if (dreamManagerService != null && !dreamManagerService.isDreaming()) {
-            if (docked) {
-                Slog.i(TAG, "Activating dream while docked.");
+        try {
+            IDreamManager dreamManagerService = IDreamManager.Stub.asInterface(
+                    ServiceManager.getService(DreamService.DREAM_SERVICE));
+            if (dreamManagerService != null && !dreamManagerService.isDreaming()) {
+                if (docked) {
+                    Slog.i(TAG, "Activating dream while docked.");
 
-                // Wake up.
-                // The power manager will wake up the system automatically when it starts
-                // receiving power from a dock but there is a race between that happening
-                // and the UI mode manager starting a dream.  We want the system to already
-                // be awake by the time this happens.  Otherwise the dream may not start.
-                PowerManager powerManager =
-                        context.getSystemService(PowerManager.class);
-                powerManager.wakeUp(SystemClock.uptimeMillis(),
-                        PowerManager.WAKE_REASON_PLUGGED_IN,
-                        "android.service.dreams:DREAM");
-            } else {
-                Slog.i(TAG, "Activating dream by user request.");
+                    // Wake up.
+                    // The power manager will wake up the system automatically when it starts
+                    // receiving power from a dock but there is a race between that happening
+                    // and the UI mode manager starting a dream.  We want the system to already
+                    // be awake by the time this happens.  Otherwise the dream may not start.
+                    PowerManager powerManager =
+                            context.getSystemService(PowerManager.class);
+                    powerManager.wakeUp(SystemClock.uptimeMillis(),
+                            PowerManager.WAKE_REASON_PLUGGED_IN,
+                            "android.service.dreams:DREAM");
+                } else {
+                    Slog.i(TAG, "Activating dream by user request.");
+                }
+
+                // Dream.
+                dreamManagerService.dream();
             }
-
-            // Dream.
-            dreamManagerService.requestDream();
+        } catch (RemoteException ex) {
+            Slog.e(TAG, "Could not start dream when docked.", ex);
         }
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 455f258..6c61d4bd 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1098,6 +1098,10 @@
                 mInputQueueCallback.onInputQueueCreated(mInputQueue);
             }
         }
+
+        // Update the last resource config in case the resource configuration was changed while
+        // activity relaunched.
+        mLastConfigurationFromResources.setTo(getConfiguration());
     }
 
     private Configuration getConfiguration() {
@@ -8540,6 +8544,10 @@
         if (mLocalSyncState != LOCAL_SYNC_NONE) {
             writer.println(innerPrefix + "mLocalSyncState=" + mLocalSyncState);
         }
+        writer.println(innerPrefix + "mLastReportedMergedConfiguration="
+                + mLastReportedMergedConfiguration);
+        writer.println(innerPrefix + "mLastConfigurationFromResources="
+                + mLastConfigurationFromResources);
         writer.println(innerPrefix + "mIsAmbientMode="  + mIsAmbientMode);
         writer.println(innerPrefix + "mUnbufferedInputSource="
                 + Integer.toHexString(mUnbufferedInputSource));
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index da786f3..b339d76 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9358,10 +9358,11 @@
                 // - The deleted text is at the end of the text
                 //     e.g. "one [deleted]" -> "one |" -> "one|"
                 // (The pipe | indicates the cursor position.)
-                while (start > 0 && TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)) {
+                do {
                     start -= Character.charCount(codePointBeforeStart);
+                    if (start == 0) break;
                     codePointBeforeStart = Character.codePointBefore(mText, start);
-                }
+                } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
             } else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
                     && (TextUtils.isWhitespace(codePointBeforeStart)
                             || TextUtils.isPunctuation(codePointBeforeStart))) {
@@ -9373,11 +9374,11 @@
                 // - The deleted text is at the start of the text
                 //     e.g. "[deleted] two" -> "| two" -> "|two"
                 // (The pipe | indicates the cursor position.)
-                while (end < mText.length()
-                        && TextUtils.isWhitespaceExceptNewline(codePointAtEnd)) {
+                do {
                     end += Character.charCount(codePointAtEnd);
+                    if (end == mText.length()) break;
                     codePointAtEnd = Character.codePointAt(mText, end);
-                }
+                } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
             }
         }
 
@@ -9487,11 +9488,19 @@
         }
 
         int endOffset = startOffset;
-        while (startOffset > 0 && Character.isWhitespace(mText.charAt(startOffset - 1))) {
-            startOffset--;
+        while (startOffset > 0) {
+            int codePointBeforeStart = Character.codePointBefore(mText, startOffset);
+            if (!TextUtils.isWhitespace(codePointBeforeStart)) {
+                break;
+            }
+            startOffset -= Character.charCount(codePointBeforeStart);
         }
-        while (endOffset < mText.length() && Character.isWhitespace(mText.charAt(endOffset))) {
-            endOffset++;
+        while (endOffset < mText.length()) {
+            int codePointAtEnd = Character.codePointAt(mText, endOffset);
+            if (!TextUtils.isWhitespace(codePointAtEnd)) {
+                break;
+            }
+            endOffset += Character.charCount(codePointAtEnd);
         }
         if (startOffset < endOffset) {
             getEditableText().delete(startOffset, endOffset);
diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl
index 3c7cd02..36eaf49 100644
--- a/core/java/android/window/IWindowOrganizerController.aidl
+++ b/core/java/android/window/IWindowOrganizerController.aidl
@@ -51,16 +51,19 @@
             in IWindowContainerTransactionCallback callback);
 
     /**
-     * Starts a transition.
+     * Starts a new transition.
      * @param type The transition type.
-     * @param transitionToken A token associated with the transition to start. If null, a new
-     *                        transition will be created of the provided type.
      * @param t Operations that are part of the transition.
-     * @return a token representing the transition. This will just be transitionToken if it was
-     *         non-null.
+     * @return a token representing the transition.
      */
-    IBinder startTransition(int type, in @nullable IBinder transitionToken,
-            in @nullable WindowContainerTransaction t);
+    IBinder startNewTransition(int type, in @nullable WindowContainerTransaction t);
+
+    /**
+     * Starts the given transition.
+     * @param transitionToken A token associated with the transition to start.
+     * @param t Operations that are part of the transition.
+     */
+    oneway void startTransition(IBinder transitionToken, in @nullable WindowContainerTransaction t);
 
     /**
      * Starts a legacy transition.
diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java
index 4ea5ea5..2a80d02 100644
--- a/core/java/android/window/WindowOrganizer.java
+++ b/core/java/android/window/WindowOrganizer.java
@@ -84,9 +84,8 @@
     }
 
     /**
-     * Start a transition.
+     * Starts a new transition, don't use this to start an already created one.
      * @param type The type of the transition. This is ignored if a transitionToken is provided.
-     * @param transitionToken An existing transition to start. If null, a new transition is created.
      * @param t The set of window operations that are part of this transition.
      * @return A token identifying the transition. This will be the same as transitionToken if it
      *         was provided.
@@ -94,10 +93,24 @@
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
     @NonNull
-    public IBinder startTransition(int type, @Nullable IBinder transitionToken,
+    public IBinder startNewTransition(int type, @Nullable WindowContainerTransaction t) {
+        try {
+            return getWindowOrganizerController().startNewTransition(type, t);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Starts an already created transition.
+     * @param transitionToken An existing transition to start.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+    public void startTransition(@NonNull IBinder transitionToken,
             @Nullable WindowContainerTransaction t) {
         try {
-            return getWindowOrganizerController().startTransition(type, transitionToken, t);
+            getWindowOrganizerController().startTransition(transitionToken, t);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 1ec5325..4f74ca7 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -86,7 +86,6 @@
     private final ChooserActivityLogger mChooserActivityLogger;
 
     private int mNumShortcutResults = 0;
-    private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
     private boolean mApplySharingAppLimits;
 
     // Reserve spots for incoming direct share targets by adding placeholders
@@ -265,31 +264,20 @@
             return;
         }
 
-        if (!(info instanceof DisplayResolveInfo)) {
-            holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
-            holder.bindIcon(info);
-
-            if (info instanceof SelectableTargetInfo) {
-                // direct share targets should append the application name for a better readout
-                DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
-                CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
-                CharSequence extendedInfo = info.getExtendedInfo();
-                String contentDescription = String.join(" ", info.getDisplayLabel(),
-                        extendedInfo != null ? extendedInfo : "", appName);
-                holder.updateContentDescription(contentDescription);
-            }
-        } else {
+        holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+        holder.bindIcon(info);
+        if (info instanceof SelectableTargetInfo) {
+            // direct share targets should append the application name for a better readout
+            DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+            CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
+            CharSequence extendedInfo = info.getExtendedInfo();
+            String contentDescription = String.join(" ", info.getDisplayLabel(),
+                    extendedInfo != null ? extendedInfo : "", appName);
+            holder.updateContentDescription(contentDescription);
+        } else if (info instanceof DisplayResolveInfo) {
             DisplayResolveInfo dri = (DisplayResolveInfo) info;
-            holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
-            LoadIconTask task = mIconLoaders.get(dri);
-            if (task == null) {
-                task = new LoadIconTask(dri, holder);
-                mIconLoaders.put(dri, task);
-                task.execute();
-            } else {
-                // The holder was potentially changed as the underlying items were
-                // reshuffled, so reset the target holder
-                task.setViewHolder(holder);
+            if (!dri.hasDisplayIcon()) {
+                loadIcon(dri);
             }
         }
 
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index c8bc204..822393f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -55,6 +55,7 @@
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -1475,14 +1476,21 @@
                 mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
         boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
 
-        ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
-        DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+        final ResolverListAdapter inactiveAdapter =
+                mMultiProfilePagerAdapter.getInactiveListAdapter();
+        final DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
 
         // Load the icon asynchronously
         ImageView icon = findViewById(R.id.icon);
-        ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
-                        otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
-        iconTask.execute();
+        inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) {
+            @Override
+            protected void onPostExecute(Drawable drawable) {
+                if (!isDestroyed()) {
+                    otherProfileResolveInfo.setDisplayIcon(drawable);
+                    new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+                }
+            }
+        }.execute();
 
         ((TextView) findViewById(R.id.open_cross_profile)).setText(
                 getResources().getString(
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 66fff5c..f6075b0 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -58,7 +58,10 @@
 import com.android.internal.app.chooser.TargetInfo;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class ResolverListAdapter extends BaseAdapter {
     private static final String TAG = "ResolverListAdapter";
@@ -87,6 +90,8 @@
     private Runnable mPostListReadyRunnable;
     private final boolean mIsAudioCaptureDevice;
     private boolean mIsTabLoaded;
+    private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
+    private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
 
     public ResolverListAdapter(Context context, List<Intent> payloadIntents,
             Intent[] initialIntents, List<ResolveInfo> rList,
@@ -636,26 +641,47 @@
         if (info == null) {
             holder.icon.setImageDrawable(
                     mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+            holder.bindLabel("", "", false);
             return;
         }
 
-        if (info instanceof DisplayResolveInfo
-                && !((DisplayResolveInfo) info).hasDisplayLabel()) {
-            getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
-        } else {
-            holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
-        }
-
-        if (info instanceof DisplayResolveInfo
-                && !((DisplayResolveInfo) info).hasDisplayIcon()) {
-            new LoadIconTask((DisplayResolveInfo) info, holder).execute();
-        } else {
+        if (info instanceof DisplayResolveInfo) {
+            DisplayResolveInfo dri = (DisplayResolveInfo) info;
+            boolean hasLabel = dri.hasDisplayLabel();
+            holder.bindLabel(
+                    dri.getDisplayLabel(),
+                    dri.getExtendedInfo(),
+                    hasLabel && alwaysShowSubLabel());
             holder.bindIcon(info);
+            if (!hasLabel) {
+                loadLabel(dri);
+            }
+            if (!dri.hasDisplayIcon()) {
+                loadIcon(dri);
+            }
         }
     }
 
-    protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
-        return new LoadLabelTask(info, holder);
+    protected final void loadIcon(DisplayResolveInfo info) {
+        LoadIconTask task = mIconLoaders.get(info);
+        if (task == null) {
+            task = new LoadIconTask((DisplayResolveInfo) info);
+            mIconLoaders.put(info, task);
+            task.execute();
+        }
+    }
+
+    private void loadLabel(DisplayResolveInfo info) {
+        LoadLabelTask task = mLabelLoaders.get(info);
+        if (task == null) {
+            task = createLoadLabelTask(info);
+            mLabelLoaders.put(info, task);
+            task.execute();
+        }
+    }
+
+    protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+        return new LoadLabelTask(info);
     }
 
     public void onDestroy() {
@@ -666,6 +692,16 @@
         if (mResolverListController != null) {
             mResolverListController.destroy();
         }
+        cancelTasks(mIconLoaders.values());
+        cancelTasks(mLabelLoaders.values());
+        mIconLoaders.clear();
+        mLabelLoaders.clear();
+    }
+
+    private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
+        for (T task: tasks) {
+            task.cancel(false);
+        }
     }
 
     private static ColorMatrixColorFilter getSuspendedColorMatrix() {
@@ -883,11 +919,9 @@
 
     protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
         private final DisplayResolveInfo mDisplayResolveInfo;
-        private final ViewHolder mHolder;
 
-        protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+        protected LoadLabelTask(DisplayResolveInfo dri) {
             mDisplayResolveInfo = dri;
-            mHolder = holder;
         }
 
         @Override
@@ -925,21 +959,22 @@
 
         @Override
         protected void onPostExecute(CharSequence[] result) {
+            if (mDisplayResolveInfo.hasDisplayLabel()) {
+                return;
+            }
             mDisplayResolveInfo.setDisplayLabel(result[0]);
             mDisplayResolveInfo.setExtendedInfo(result[1]);
-            mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+            notifyDataSetChanged();
         }
     }
 
     class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
         protected final DisplayResolveInfo mDisplayResolveInfo;
         private final ResolveInfo mResolveInfo;
-        private ViewHolder mHolder;
 
-        LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+        LoadIconTask(DisplayResolveInfo dri) {
             mDisplayResolveInfo = dri;
             mResolveInfo = dri.getResolveInfo();
-            mHolder = holder;
         }
 
         @Override
@@ -953,17 +988,9 @@
                 mResolverListCommunicator.updateProfileViewButton();
             } else if (!mDisplayResolveInfo.hasDisplayIcon()) {
                 mDisplayResolveInfo.setDisplayIcon(d);
-                mHolder.bindIcon(mDisplayResolveInfo);
-                // Notify in case view is already bound to resolve the race conditions on
-                // low end devices
                 notifyDataSetChanged();
             }
         }
-
-        public void setViewHolder(ViewHolder holder) {
-            mHolder = holder;
-            mHolder.bindIcon(mDisplayResolveInfo);
-        }
     }
 
     /**
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 65c2d00..55a26fe 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -26,6 +26,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.PropertyInvalidatedCache;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.PasswordMetrics;
@@ -1784,4 +1785,16 @@
             re.rethrowFromSystemServer();
         }
     }
+
+    public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+        getLockSettingsInternal().unlockUserKeyIfUnsecured(userId);
+    }
+
+    public void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+        getLockSettingsInternal().createNewUser(userId, userSerialNumber);
+    }
+
+    public void removeUser(@UserIdInt int userId) {
+        getLockSettingsInternal().removeUser(userId);
+    }
 }
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 0a2c18f8..5b08bb1 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.admin.PasswordMetrics;
 
 import java.lang.annotation.Retention;
@@ -53,6 +54,37 @@
     // TODO(b/183140900) split store escrow key errors into detailed ones.
 
     /**
+     * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e.
+     * doesn't have an LSKF.
+     * <p>
+     * This doesn't throw an exception on failure; whether the storage has been unlocked can be
+     * determined by {@link StorageManager#isUserKeyUnlocked()}.
+     *
+     * @param userId the ID of the user whose storage to unlock
+     */
+    public abstract void unlockUserKeyIfUnsecured(@UserIdInt int userId);
+
+    /**
+     * Creates the locksettings state for a new user.
+     * <p>
+     * This includes creating a synthetic password and protecting it with an empty LSKF.
+     *
+     * @param userId the ID of the new user
+     * @param userSerialNumber the serial number of the new user
+     */
+    public abstract void createNewUser(@UserIdInt int userId, int userSerialNumber);
+
+    /**
+     * Removes the locksettings state for the given user.
+     * <p>
+     * This includes removing the user's synthetic password and any protectors that are protecting
+     * it.
+     *
+     * @param userId the ID of the user being removed
+     */
+    public abstract void removeUser(@UserIdInt int userId);
+
+    /**
      * Create an escrow token for the current user, which can later be used to unlock FBE
      * or change user password.
      *
diff --git a/core/jni/android_media_AudioVolumeGroups.cpp b/core/jni/android_media_AudioVolumeGroups.cpp
index 7098451..1252e89 100644
--- a/core/jni/android_media_AudioVolumeGroups.cpp
+++ b/core/jni/android_media_AudioVolumeGroups.cpp
@@ -94,6 +94,11 @@
     for (size_t j = 0; j < static_cast<size_t>(numAttributes); j++) {
         auto attributes = group.getAudioAttributes()[j];
 
+        // Native & Java audio attributes default initializers are not aligned for the source.
+        // Given the volume group class concerns only playback, this field must be equal to the
+        // default java initializer.
+        attributes.source = AUDIO_SOURCE_INVALID;
+
         jStatus = JNIAudioAttributeHelper::nativeToJava(env, &jAudioAttribute, attributes);
         if (jStatus != AUDIO_JAVA_SUCCESS) {
             goto exit;
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index b9d5ee4..9501c8d 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1252,6 +1252,20 @@
     return fd;
 }
 
+void android_os_Process_freezeCgroupUID(JNIEnv* env, jobject clazz, jint uid, jboolean freeze) {
+    bool success = true;
+
+    if (freeze) {
+        success = SetUserProfiles(uid, {"Frozen"});
+    } else {
+        success = SetUserProfiles(uid, {"Unfrozen"});
+    }
+
+    if (!success) {
+        jniThrowRuntimeException(env, "Could not apply user profile");
+    }
+}
+
 static const JNINativeMethod methods[] = {
         {"getUidForName", "(Ljava/lang/String;)I", (void*)android_os_Process_getUidForName},
         {"getGidForName", "(Ljava/lang/String;)I", (void*)android_os_Process_getGidForName},
@@ -1293,6 +1307,7 @@
         {"killProcessGroup", "(II)I", (void*)android_os_Process_killProcessGroup},
         {"removeAllProcessGroups", "()V", (void*)android_os_Process_removeAllProcessGroups},
         {"nativePidFdOpen", "(II)I", (void*)android_os_Process_nativePidFdOpen},
+        {"freezeCgroupUid", "(IZ)V", (void*)android_os_Process_freezeCgroupUID},
 };
 
 int register_android_os_Process(JNIEnv* env)
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ded1b89..f0b1b2a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3051,6 +3051,10 @@
     <permission android:name="android.permission.QUERY_ADMIN_POLICY"
                 android:protectionLevel="signature|role" />
 
+    <!-- @SystemApi @hide Allows an application to exempt apps from platform restrictions.-->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"
+                android:protectionLevel="signature|role" />
+
     <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
     <permission android:name="android.permission.PROVISION_DEMO_DEVICE"
                 android:protectionLevel="signature|setup" />
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
index 56a7070..2861428 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
@@ -46,14 +46,14 @@
     }
 
     @Override
-    protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
-        return new LoadLabelWrapperTask(info, holder);
+    protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+        return new LoadLabelWrapperTask(info);
     }
 
     class LoadLabelWrapperTask extends LoadLabelTask {
 
-        protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
-            super(dri, holder);
+        protected LoadLabelWrapperTask(DisplayResolveInfo dri) {
+            super(dri);
         }
 
         @Override
diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java
index c2cd6ff..f507d76 100644
--- a/keystore/java/android/security/KeyStore2.java
+++ b/keystore/java/android/security/KeyStore2.java
@@ -32,6 +32,7 @@
 import android.util.Log;
 
 import java.util.Calendar;
+import java.util.Objects;
 
 /**
  * @hide This should not be made public in its present form because it
@@ -137,13 +138,13 @@
         return new KeyStore2();
     }
 
-    private synchronized IKeystoreService getService(boolean retryLookup) {
+    @NonNull private synchronized IKeystoreService getService(boolean retryLookup) {
         if (mBinder == null || retryLookup) {
             mBinder = IKeystoreService.Stub.asInterface(ServiceManager
                     .getService(KEYSTORE2_SERVICE_NAME));
             Binder.allowBlocking(mBinder.asBinder());
         }
-        return mBinder;
+        return Objects.requireNonNull(mBinder);
     }
 
     void delete(KeyDescriptor descriptor) throws KeyStoreException {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
index b631999..4e73bd9 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
@@ -18,13 +18,19 @@
 
 import android.annotation.NonNull;
 import android.security.KeyStoreSecurityLevel;
+import android.security.keymaster.KeymasterDefs;
 import android.security.keystore.KeyProperties;
+import android.system.keystore2.Authorization;
 import android.system.keystore2.KeyDescriptor;
 import android.system.keystore2.KeyMetadata;
 
+import java.security.AlgorithmParameters;
+import java.security.NoSuchAlgorithmException;
 import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
 import java.security.spec.ECParameterSpec;
 import java.security.spec.ECPoint;
+import java.security.spec.InvalidParameterSpecException;
 
 /**
  * {@link ECPublicKey} backed by keystore.
@@ -56,11 +62,45 @@
         }
     }
 
+    private static String getEcCurveFromKeymaster(int ecCurve) {
+        switch (ecCurve) {
+            case android.hardware.security.keymint.EcCurve.P_224:
+                return "secp224r1";
+            case android.hardware.security.keymint.EcCurve.P_256:
+                return "secp256r1";
+            case android.hardware.security.keymint.EcCurve.P_384:
+                return "secp384r1";
+            case android.hardware.security.keymint.EcCurve.P_521:
+                return "secp521r1";
+        }
+        return "";
+    }
+
+    private ECParameterSpec getCurveSpec(String name)
+            throws NoSuchAlgorithmException, InvalidParameterSpecException {
+        AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
+        parameters.init(new ECGenParameterSpec(name));
+        return parameters.getParameterSpec(ECParameterSpec.class);
+    }
+
     @Override
     public AndroidKeyStorePrivateKey getPrivateKey() {
+        ECParameterSpec params = mParams;
+        for (Authorization a : getAuthorizations()) {
+            try {
+                if (a.keyParameter.tag == KeymasterDefs.KM_TAG_EC_CURVE) {
+                    params = getCurveSpec(getEcCurveFromKeymaster(
+                            a.keyParameter.value.getEcCurve()));
+                    break;
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("Unable to parse EC curve "
+                        + a.keyParameter.value.getEcCurve());
+            }
+        }
         return new AndroidKeyStoreECPrivateKey(
                 getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(),
-                getSecurityLevel(), mParams);
+                getSecurityLevel(), params);
     }
 
     @Override
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
index b1338d1..4caa47f 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
@@ -31,6 +31,8 @@
 import java.security.ProviderException;
 import java.security.PublicKey;
 import java.security.SecureRandom;
+import java.security.interfaces.ECKey;
+import java.security.interfaces.XECKey;
 import java.security.spec.AlgorithmParameterSpec;
 import java.util.ArrayList;
 import java.util.List;
@@ -132,6 +134,15 @@
             throw new InvalidKeyException("key == null");
         } else if (!(key instanceof PublicKey)) {
             throw new InvalidKeyException("Only public keys supported. Key: " + key);
+        } else if (!(mKey instanceof ECKey && key instanceof ECKey)
+                && !(mKey instanceof XECKey && key instanceof XECKey)) {
+            throw new InvalidKeyException(
+                    "Public and Private key should be of the same type:");
+        } else if (mKey instanceof ECKey
+                && !((ECKey) key).getParams().getCurve()
+                .equals(((ECKey) mKey).getParams().getCurve())) {
+            throw new InvalidKeyException(
+                    "Public and Private key parameters should be same.");
         } else if (!lastPhase) {
             throw new IllegalStateException(
                     "Only one other party supported. lastPhase must be set to true.");
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
index 42589640..e392c8d 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
@@ -22,16 +22,18 @@
 import android.system.keystore2.KeyDescriptor;
 
 import java.security.PrivateKey;
-import java.security.interfaces.EdECKey;
+import java.security.interfaces.XECPrivateKey;
 import java.security.spec.NamedParameterSpec;
+import java.util.Optional;
 
 /**
  * X25519 Private Key backed by Keystore.
- * instance of {@link PrivateKey} and {@link EdECKey}
+ * instance of {@link PrivateKey} and {@link XECPrivateKey}
  *
  * @hide
  */
-public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey {
+public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey
+        implements XECPrivateKey {
     public AndroidKeyStoreXDHPrivateKey(
             @NonNull KeyDescriptor descriptor, long keyId,
             @NonNull Authorization[] authorizations,
@@ -44,4 +46,12 @@
     public NamedParameterSpec getParams() {
         return NamedParameterSpec.X25519;
     }
+
+    @Override
+    public Optional<byte[]> getScalar() {
+        /* An empty Optional if the scalar cannot be extracted (e.g. if the provider is a hardware
+         * token and the private key is not allowed to leave the crypto boundary).
+         */
+        return Optional.empty();
+    }
 }
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index afd3aac..70755e6 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -119,7 +119,7 @@
 
     <!-- Temporarily extending the background to show an edu text hint for opening the menu -->
     <FrameLayout
-        android:id="@+id/tv_pip_menu_edu_text_container"
+        android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_below="@+id/tv_pip"
@@ -127,23 +127,8 @@
         android:layout_alignStart="@+id/tv_pip"
         android:layout_alignEnd="@+id/tv_pip"
         android:background="@color/tv_pip_menu_background"
-        android:clipChildren="true">
-
-        <TextView
-            android:id="@+id/tv_pip_menu_edu_text"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/pip_menu_edu_text_view_height"
-            android:layout_gravity="bottom|center"
-            android:gravity="center"
-            android:clickable="false"
-            android:paddingBottom="@dimen/pip_menu_border_width"
-            android:text="@string/pip_edu_text"
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:marqueeRepeatLimit="1"
-            android:scrollHorizontally="true"
-            android:textAppearance="@style/TvPipEduText"/>
-    </FrameLayout>
+        android:paddingBottom="@dimen/pip_menu_border_width"
+        android:paddingTop="@dimen/pip_menu_border_width"/>
 
     <!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
     <View
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index b45b9ec..9833a88 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -41,8 +41,10 @@
     <dimen name="pip_menu_edu_text_view_height">24dp</dimen>
     <dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
     <dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
-    <integer name="pip_edu_text_show_duration_ms">10500</integer>
-    <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
-    <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
+    <integer name="pip_edu_text_scroll_times">2</integer>
+    <integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
+    <integer name="pip_edu_text_start_scroll_delay">2000</integer>
+    <integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
+    <integer name="pip_edu_text_view_exit_animation_duration">300</integer>
 </resources>
 
diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml
index 2b7a13e..8f806cf 100644
--- a/libs/WindowManager/Shell/res/values/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values/strings_tv.xml
@@ -42,8 +42,8 @@
 
     <!-- Educative text instructing the user to double press the HOME button to access the pip
         controls menu [CHAR LIMIT=50] -->
-    <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
-        controls </string>
+    <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
+        controls</string>
 
     <!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
     <string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java
new file mode 100644
index 0000000..e029358
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java
@@ -0,0 +1,57 @@
+/*
+ * 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.wm.shell.common;
+
+import static android.content.Intent.EXTRA_DOCK_STATE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Provides information about the docked state of the device.
+ */
+@WMSingleton
+public class DockStateReader {
+
+    private static final IntentFilter DOCK_INTENT_FILTER = new IntentFilter(
+            Intent.ACTION_DOCK_EVENT);
+
+    private final Context mContext;
+
+    @Inject
+    public DockStateReader(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * @return True if the device is docked and false otherwise.
+     */
+    public boolean isDocked() {
+        Intent dockStatus = mContext.registerReceiver(/* receiver */ null, DOCK_INTENT_FILTER);
+        if (dockStatus != null) {
+            int dockState = dockStatus.getIntExtra(EXTRA_DOCK_STATE,
+                    Intent.EXTRA_DOCK_STATE_UNDOCKED);
+            return dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
+        }
+        return false;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 235fd9c..6627de5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -37,6 +37,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
@@ -109,6 +110,7 @@
     private final SyncTransactionQueue mSyncQueue;
     private final ShellExecutor mMainExecutor;
     private final Lazy<Transitions> mTransitionsLazy;
+    private final DockStateReader mDockStateReader;
 
     private CompatUICallback mCallback;
 
@@ -127,7 +129,8 @@
             DisplayImeController imeController,
             SyncTransactionQueue syncQueue,
             ShellExecutor mainExecutor,
-            Lazy<Transitions> transitionsLazy) {
+            Lazy<Transitions> transitionsLazy,
+            DockStateReader dockStateReader) {
         mContext = context;
         mShellController = shellController;
         mDisplayController = displayController;
@@ -138,6 +141,7 @@
         mTransitionsLazy = transitionsLazy;
         mCompatUIHintsState = new CompatUIHintsState();
         shellInit.addInitCallback(this::onInit, this);
+        mDockStateReader = dockStateReader;
     }
 
     private void onInit() {
@@ -315,7 +319,8 @@
         return new LetterboxEduWindowManager(context, taskInfo,
                 mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
                 mTransitionsLazy.get(),
-                this::onLetterboxEduDismissed);
+                this::onLetterboxEduDismissed,
+                mDockStateReader);
     }
 
     private void onLetterboxEduDismissed() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 35f1038..867d0ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -34,6 +34,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
 import com.android.wm.shell.transition.Transitions;
@@ -88,19 +89,21 @@
      */
     private final int mDialogVerticalMargin;
 
+    private final DockStateReader mDockStateReader;
+
     public LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
             SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
             DisplayLayout displayLayout, Transitions transitions,
-            Runnable onDismissCallback) {
+            Runnable onDismissCallback, DockStateReader dockStateReader) {
         this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
-                onDismissCallback, new LetterboxEduAnimationController(context));
+                onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
     }
 
     @VisibleForTesting
     LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
             SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
             DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
-            LetterboxEduAnimationController animationController) {
+            LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
         mTransitions = transitions;
         mOnDismissCallback = onDismissCallback;
@@ -111,6 +114,7 @@
                 Context.MODE_PRIVATE);
         mDialogVerticalMargin = (int) mContext.getResources().getDimension(
                 R.dimen.letterbox_education_dialog_margin);
+        mDockStateReader = dockStateReader;
     }
 
     @Override
@@ -130,13 +134,15 @@
 
     @Override
     protected boolean eligibleToShowLayout() {
+        // - The letterbox education should not be visible if the device is docked.
         // - If taskbar education is showing, the letterbox education shouldn't be shown for the
         //   given task until the taskbar education is dismissed and the compat info changes (then
         //   the controller will create a new instance of this class since this one isn't eligible).
         // - If the layout isn't null then it was previously showing, and we shouldn't check if the
         //   user has seen the letterbox education before.
-        return mEligibleForLetterboxEducation && !isTaskbarEduShowing() && (mLayout != null
-                || !getHasSeenLetterboxEducation());
+        return mEligibleForLetterboxEducation && !isTaskbarEduShowing()
+                && (mLayout != null || !getHasSeenLetterboxEducation())
+                && !mDockStateReader.isDocked();
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index c4accde..2214a98 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -46,6 +46,7 @@
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -69,7 +70,6 @@
 import com.android.wm.shell.freeform.FreeformComponents;
 import com.android.wm.shell.fullscreen.FullscreenTaskListener;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
@@ -192,33 +192,16 @@
 
     @WMSingleton
     @Provides
-    static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
-            Context context,
-            ShellInit shellInit,
-            ShellCommandHandler shellCommandHandler,
-            SyncTransactionQueue syncTransactionQueue,
-            DisplayController displayController,
-            DisplayInsetsController displayInsetsController,
-            Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<RecentTasksController> recentTasksOptional,
-            @ShellMainThread ShellExecutor mainExecutor,
-            @ShellMainThread Handler mainHandler
-    ) {
-        return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
-                syncTransactionQueue, displayController, displayInsetsController,
-                unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
-    }
-
-    @WMSingleton
-    @Provides
     static CompatUIController provideCompatUIController(Context context,
             ShellInit shellInit,
             ShellController shellController,
             DisplayController displayController, DisplayInsetsController displayInsetsController,
             DisplayImeController imeController, SyncTransactionQueue syncQueue,
-            @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) {
+            @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy,
+            DockStateReader dockStateReader) {
         return new CompatUIController(context, shellInit, shellController, displayController,
-                displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy);
+                displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy,
+                dockStateReader);
     }
 
     @WMSingleton
@@ -782,7 +765,6 @@
             DisplayInsetsController displayInsetsController,
             DragAndDropController dragAndDropController,
             ShellTaskOrganizer shellTaskOrganizer,
-            KidsModeTaskOrganizer kidsModeTaskOrganizer,
             Optional<BubbleController> bubblesOptional,
             Optional<SplitScreenController> splitScreenOptional,
             Optional<Pip> pipOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 37a50b6..47b6659 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -56,6 +56,7 @@
 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
 import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
 import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
@@ -620,6 +621,28 @@
     }
 
     //
+    // Kids mode
+    //
+    @WMSingleton
+    @Provides
+    static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
+            Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            SyncTransactionQueue syncTransactionQueue,
+            DisplayController displayController,
+            DisplayInsetsController displayInsetsController,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
+            Optional<RecentTasksController> recentTasksOptional,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler
+    ) {
+        return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
+                syncTransactionQueue, displayController, displayInsetsController,
+                unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
+    }
+
+    //
     // Misc
     //
 
@@ -630,6 +653,7 @@
     @Provides
     static Object provideIndependentShellComponentsToCreate(
             DefaultMixedHandler defaultMixedHandler,
+            KidsModeTaskOrganizer kidsModeTaskOrganizer,
             Optional<DesktopModeController> desktopModeController) {
         return new Object();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 4def15d..2624ee5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -59,10 +59,15 @@
     /**
      * Sets listener to get pinned stack animation callbacks.
      */
-    oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3;
+    oneway void setPipAnimationListener(IPipAnimationListener listener) = 3;
 
     /**
      * Sets the shelf height and visibility.
      */
     oneway void setShelfHeight(boolean visible, int shelfHeight) = 4;
+
+    /**
+     * Sets the next pip animation type to be the alpha animation.
+     */
+    oneway void setPipAnimationTypeToAlpha() = 5;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index c06881a..72b9dd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -51,15 +51,6 @@
     }
 
     /**
-     * Sets both shelf visibility and its height.
-     *
-     * @param visible visibility of shelf.
-     * @param height  to specify the height for shelf.
-     */
-    default void setShelfHeight(boolean visible, int height) {
-    }
-
-    /**
      * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
      *
      * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
@@ -68,14 +59,6 @@
     default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
 
     /**
-     * Set the pinned stack with {@link PipAnimationController.AnimationType}
-     *
-     * @param animationType The pre-defined {@link PipAnimationController.AnimationType}
-     */
-    default void setPinnedStackAnimationType(int animationType) {
-    }
-
-    /**
      * Called when showing Pip menu.
      */
     default void showPictureInPictureMenu() {}
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 af47666..3345b1b 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
@@ -23,6 +23,7 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
@@ -1065,13 +1066,6 @@
         }
 
         @Override
-        public void setShelfHeight(boolean visible, int height) {
-            mMainExecutor.execute(() -> {
-                PipController.this.setShelfHeight(visible, height);
-            });
-        }
-
-        @Override
         public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
             mMainExecutor.execute(() -> {
                 PipController.this.setOnIsInPipStateChangedListener(callback);
@@ -1079,13 +1073,6 @@
         }
 
         @Override
-        public void setPinnedStackAnimationType(int animationType) {
-            mMainExecutor.execute(() -> {
-                PipController.this.setPinnedStackAnimationType(animationType);
-            });
-        }
-
-        @Override
         public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) {
             mMainExecutor.execute(() -> {
                 mPipBoundsState.addPipExclusionBoundsChangeCallback(listener);
@@ -1178,8 +1165,8 @@
         }
 
         @Override
-        public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
-            executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
+        public void setPipAnimationListener(IPipAnimationListener listener) {
+            executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener",
                     (controller) -> {
                         if (listener != null) {
                             mListener.register(listener);
@@ -1188,5 +1175,13 @@
                         }
                     });
         }
+
+        @Override
+        public void setPipAnimationTypeToAlpha() {
+            executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha",
+                    (controller) -> {
+                        controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA);
+                    });
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
index acc0caf..d7d335b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
@@ -43,16 +43,16 @@
      * <p>MAX - maximum allowed screen size</p>
      */
     @IntDef(value = {
-        SIZE_SPEC_CUSTOM,
         SIZE_SPEC_DEFAULT,
-        SIZE_SPEC_MAX
+        SIZE_SPEC_MAX,
+        SIZE_SPEC_CUSTOM
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface PipSizeSpec {}
 
-    static final int SIZE_SPEC_CUSTOM = 2;
     static final int SIZE_SPEC_DEFAULT = 0;
     static final int SIZE_SPEC_MAX = 1;
+    static final int SIZE_SPEC_CUSTOM = 2;
 
     /**
      * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 4c26224..3e8de45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -127,7 +127,7 @@
     private int mPipForceCloseDelay;
 
     private int mResizeAnimationDuration;
-    private int mEduTextWindowExitAnimationDurationMs;
+    private int mEduTextWindowExitAnimationDuration;
 
     public static Pip create(
             Context context,
@@ -371,10 +371,10 @@
     }
 
     @Override
-    public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
-        mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
+    public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+        mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
                 animationDuration, rect -> mTvPipMenuController.updateExpansionState());
-        mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
+        mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
     }
 
     /**
@@ -411,7 +411,7 @@
 
     @Override
     public void closeEduText() {
-        updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
+        updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
     }
 
     private void registerSessionListenerForCurrentUser() {
@@ -453,27 +453,30 @@
     }
 
     @Override
-    public void onPipTransitionStarted(int direction, Rect pipBounds) {
+    public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.notifyPipAnimating(true);
+                "%s: onPipTransition_Started(), state=%s, direction=%d",
+                TAG, stateToName(mState), direction);
     }
 
     @Override
     public void onPipTransitionCanceled(int direction) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.notifyPipAnimating(false);
+        mTvPipMenuController.onPipTransitionFinished(
+                PipAnimationController.isInPipDirection(direction));
     }
 
     @Override
     public void onPipTransitionFinished(int direction) {
-        if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
+        final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+        if (enterPipTransition && mState == STATE_NO_PIP) {
             setState(STATE_PIP);
         }
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.notifyPipAnimating(false);
+                "%s: onPipTransition_Finished(), state=%s, direction=%d",
+                TAG, stateToName(mState), direction);
+        mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
     }
 
     private void setState(@State int state) {
@@ -487,8 +490,8 @@
         final Resources res = mContext.getResources();
         mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
         mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
-        mEduTextWindowExitAnimationDurationMs =
-                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
+        mEduTextWindowExitAnimationDuration =
+                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
     }
 
     private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 176fdfe..ab7edbf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -60,9 +60,6 @@
     private final SystemWindows mSystemWindows;
     private final TvPipBoundsState mTvPipBoundsState;
     private final Handler mMainHandler;
-    private final int mPipMenuBorderWidth;
-    private final int mPipEduTextShowDurationMs;
-    private final int mPipEduTextHeight;
 
     private Delegate mDelegate;
     private SurfaceControl mLeash;
@@ -85,8 +82,6 @@
     RectF mTmpDestinationRectF = new RectF();
     Matrix mMoveTransform = new Matrix();
 
-    private final Runnable mCloseEduTextRunnable = this::closeEduText;
-
     public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
             SystemWindows systemWindows, PipMediaController pipMediaController,
             Handler mainHandler) {
@@ -109,12 +104,6 @@
 
         pipMediaController.addActionListener(this::onMediaActionsChanged);
 
-        mPipEduTextShowDurationMs = context.getResources()
-                .getInteger(R.integer.pip_edu_text_show_duration_ms);
-        mPipEduTextHeight = context.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
-        mPipMenuBorderWidth = context.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_border_width);
     }
 
     void setDelegate(Delegate delegate) {
@@ -152,15 +141,17 @@
         attachPipBackgroundView();
         attachPipMenuView();
 
-        mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
-                -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
-        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
-        mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
+        int pipEduTextHeight = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+        int pipMenuBorderWidth = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_border_width);
+        mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
+                    -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
+        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
     }
 
     private void attachPipMenuView() {
-        mPipMenuView = new TvPipMenuView(mContext);
-        mPipMenuView.setListener(this);
+        mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
         setUpViewSurfaceZOrder(mPipMenuView, 1);
         addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
         maybeUpdateMenuViewActions();
@@ -192,11 +183,15 @@
                 0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
     }
 
-    void notifyPipAnimating(boolean animating) {
-        mPipMenuView.setEduTextActive(!animating);
-        if (!animating) {
-            mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded());
-        }
+    void onPipTransitionFinished(boolean enterTransition) {
+        // There is a race between when this is called and when the last frame of the pip transition
+        // is drawn. To ensure that view updates are applied only when the animation has fully drawn
+        // and the menu view has been fully remeasured and relaid out, we add a small delay here by
+        // posting on the handler.
+        mMainHandler.post(() -> {
+            mPipMenuView.onPipTransitionFinished(
+                    enterTransition, mTvPipBoundsState.isTvPipExpanded());
+        });
     }
 
     void showMovementMenuOnly() {
@@ -219,7 +214,6 @@
         if (mPipMenuView == null) {
             return;
         }
-        maybeCloseEduText();
         maybeUpdateMenuViewActions();
         updateExpansionState();
 
@@ -232,25 +226,12 @@
         mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
     }
 
-    void onPipTransitionStarted(Rect finishBounds) {
+    void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
         if (mPipMenuView != null) {
-            mPipMenuView.onPipTransitionStarted(finishBounds);
+            mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
         }
     }
 
-    private void maybeCloseEduText() {
-        if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
-            mMainHandler.removeCallbacks(mCloseEduTextRunnable);
-            mCloseEduTextRunnable.run();
-        }
-    }
-
-    private void closeEduText() {
-        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
-        mPipMenuView.hideEduText();
-        mDelegate.closeEduText();
-    }
-
     void updateGravity(int gravity) {
         mPipMenuView.showMovementHints(gravity);
     }
@@ -332,7 +313,6 @@
     @Override
     public void detach() {
         closeMenu();
-        mMainHandler.removeCallbacks(mCloseEduTextRunnable);
         detachPipMenu();
         mLeash = null;
     }
@@ -578,6 +558,12 @@
         mDelegate.togglePipExpansion();
     }
 
+    @Override
+    public void onCloseEduText() {
+        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
+        mDelegate.closeEduText();
+    }
+
     interface Delegate {
         void movePipToFullscreen();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
new file mode 100644
index 0000000..6eef225
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -0,0 +1,280 @@
+/*
+ * 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.wm.shell.pip.tv;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER;
+import static android.view.View.GONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.Annotation;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+
+import java.util.Arrays;
+
+/**
+ * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
+ * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
+ * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
+ * not be enough space to fit the whole educational text in the available space. In such cases we
+ * apply a marquee animation to the TextView inside the drawer.
+ *
+ * The drawer is shown temporarily giving the user enough time to read it, after which it slides
+ * shut. We show the text for a duration calculated based on whether the text is marqueed or not.
+ */
+class TvPipMenuEduTextDrawer extends FrameLayout {
+    private static final String TAG = "TvPipMenuEduTextDrawer";
+
+    private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
+    private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
+    private final float mMarqueeAnimSpeed; // pixels per ms
+
+    private final Runnable mCloseDrawerRunnable = this::closeDrawer;
+    private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
+
+    private final Handler mMainHandler;
+    private final Listener mListener;
+    private final TextView mEduTextView;
+
+    TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
+        super(context, null, 0, 0);
+
+        mListener = listener;
+        mMainHandler = mainHandler;
+
+        // Taken from TextView.Marquee calculation
+        mMarqueeAnimSpeed =
+            (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
+
+        mEduTextView = new TextView(mContext);
+        setupDrawer();
+    }
+
+    private void setupDrawer() {
+        final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
+                R.dimen.pip_menu_edu_text_view_height);
+        final int marqueeRepeatLimit = mContext.getResources()
+                .getInteger(R.integer.pip_edu_text_scroll_times);
+
+        mEduTextView.setLayoutParams(
+                new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
+        mEduTextView.setGravity(CENTER);
+        mEduTextView.setClickable(false);
+        mEduTextView.setText(createEduTextString());
+        mEduTextView.setSingleLine();
+        mEduTextView.setTextAppearance(R.style.TvPipEduText);
+        mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+        mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
+        mEduTextView.setHorizontallyScrolling(true);
+        mEduTextView.setHorizontalFadingEdgeEnabled(true);
+        mEduTextView.setSelected(false);
+        addView(mEduTextView);
+
+        setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
+        setClipChildren(true);
+    }
+
+    /**
+     * Initializes the edu text. Should only be called once when the PiP is entered
+     */
+    void init() {
+        ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
+        scheduleLifecycleEvents();
+    }
+
+    private void scheduleLifecycleEvents() {
+        final int startScrollDelay = mContext.getResources().getInteger(
+                R.integer.pip_edu_text_start_scroll_delay);
+        if (isEduTextMarqueed()) {
+            mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
+        }
+        mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
+        mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
+                    new ViewTreeObserver.OnWindowAttachListener() {
+                @Override
+                public void onWindowAttached() {
+                }
+
+                @Override
+                public void onWindowDetached() {
+                    mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
+                    mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
+                    mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+                }
+            });
+    }
+
+    private int getEduTextShowDuration() {
+        int eduTextShowDuration;
+        if (isEduTextMarqueed()) {
+            // Calculate the time it takes to fully scroll the text once: time = distance / speed
+            final float singleMarqueeDuration =
+                    getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
+            // The TextView adds a delay between each marquee repetition. Take that into account
+            final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
+            // Finally, multiply by the number of times we repeat the marquee animation
+            eduTextShowDuration =
+                    (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
+        } else {
+            eduTextShowDuration = mContext.getResources()
+                    .getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
+        }
+
+        ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
+                TAG, eduTextShowDuration);
+        return eduTextShowDuration;
+    }
+
+    /**
+     * Returns true if the edu text width is bigger than the width of the text view, which indicates
+     * that the edu text will be marqueed
+     */
+    private boolean isEduTextMarqueed() {
+        final int availableWidth = (int) mEduTextView.getWidth()
+                - mEduTextView.getCompoundPaddingLeft()
+                - mEduTextView.getCompoundPaddingRight();
+        return availableWidth < getEduTextWidth();
+    }
+
+    /**
+     * Returns the width of a single marquee repetition of the edu text in pixels.
+     * This is the width from the start of the edu text to the start of the next edu
+     * text when it is marqueed.
+     *
+     * This is calculated based on the TextView.Marquee#start calculations
+     */
+    private float getMarqueeAnimEduTextLineWidth() {
+        // When the TextView has a marquee animation, it puts a gap between the text end and the
+        // start of the next edu text repetition. The space is equal to a third of the TextView
+        // width
+        final float gap = mEduTextView.getWidth() / 3.0f;
+        return getEduTextWidth() + gap;
+    }
+
+    private void startScrollEduText() {
+        ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
+                TAG, mEduTextView.getMarqueeRepeatLimit());
+        mEduTextView.setSelected(true);
+    }
+
+    /**
+     * Returns the width of the edu text irrespective of the TextView width
+     */
+    private int getEduTextWidth() {
+        return (int) mEduTextView.getLayout().getLineWidth(0);
+    }
+
+    /**
+     * Closes the edu text drawer if it hasn't been closed yet
+     */
+    void closeIfNeeded() {
+        if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
+            ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: close(), closing the edu text drawer because of user action", TAG);
+            mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+            mCloseDrawerRunnable.run();
+        } else {
+            // Do nothing, the drawer has already been closed
+        }
+    }
+
+    private void closeDrawer() {
+        ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
+        final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
+                R.integer.pip_edu_text_view_exit_animation_duration);
+        final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
+                R.integer.pip_edu_text_window_exit_animation_duration);
+
+        // Start fading out the edu text
+        mEduTextView.animate()
+                .alpha(0f)
+                .setInterpolator(TvPipInterpolators.EXIT)
+                .setDuration(eduTextFadeExitAnimationDuration)
+                .start();
+
+        // Start animation to close the drawer by animating its height to 0
+        final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0);
+        heightAnimation.setDuration(eduTextSlideExitAnimationDuration);
+        heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
+        heightAnimation.addUpdateListener(animator -> {
+            final ViewGroup.LayoutParams params = getLayoutParams();
+            params.height = (int) animator.getAnimatedValue();
+            setLayoutParams(params);
+            if (params.height == 0) {
+                setVisibility(GONE);
+            }
+        });
+        heightAnimation.start();
+
+        mListener.onCloseEduText();
+    }
+
+    /**
+     * Creates the educational text that will be displayed to the user. Here we replace the
+     * HOME annotation in the String with an icon
+     */
+    private CharSequence createEduTextString() {
+        final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
+        final SpannableString spannableString = new SpannableString(eduText);
+        Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
+                .ifPresent(annotation -> {
+                    final Drawable icon =
+                            getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
+                    if (icon != null) {
+                        icon.mutate();
+                        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+                        spannableString.setSpan(new CenteredImageSpan(icon),
+                                eduText.getSpanStart(annotation),
+                                eduText.getSpanEnd(annotation),
+                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    }
+                });
+
+        return spannableString;
+    }
+
+    /**
+     * A listener for edu text drawer event states.
+     */
+    interface Listener {
+        /**
+         *  The edu text closing impacts the size of the Picture-in-Picture window and influences
+         *  how it is positioned on the screen.
+         */
+        void onCloseEduText();
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 9cd05b0..57e95c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,18 +25,11 @@
 import static android.view.KeyEvent.KEYCODE_DPAD_UP;
 import static android.view.KeyEvent.KEYCODE_ENTER;
 
-import android.animation.ValueAnimator;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.content.Context;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.text.Annotation;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannedString;
-import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.SurfaceControl;
@@ -49,7 +42,6 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -61,7 +53,6 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -74,21 +65,16 @@
 
     private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
 
-    @Nullable
-    private Listener mListener;
+    private final Listener mListener;
 
     private final LinearLayout mActionButtonsContainer;
     private final View mMenuFrameView;
     private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
     private final View mPipFrameView;
     private final View mPipView;
-    private final TextView mEduTextView;
-    private final View mEduTextContainerView;
+    private final TvPipMenuEduTextDrawer mEduTextDrawer;
     private final int mPipMenuOuterSpace;
     private final int mPipMenuBorderWidth;
-    private final int mEduTextFadeExitAnimationDurationMs;
-    private final int mEduTextSlideExitAnimationDurationMs;
-    private int mEduTextHeight;
 
     private final ImageView mArrowUp;
     private final ImageView mArrowRight;
@@ -116,25 +102,17 @@
     private final int mResizeAnimationDuration;
 
     private final AccessibilityManager mA11yManager;
+    private final Handler mMainHandler;
 
-    public TvPipMenuView(@NonNull Context context) {
-        this(context, null);
-    }
-
-    public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+    public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
+            @NonNull Listener listener) {
+        super(context, null, 0, 0);
 
         inflate(context, R.layout.tv_pip_menu, this);
 
+        mMainHandler = mainHandler;
+        mListener = listener;
+
         mA11yManager = context.getSystemService(AccessibilityManager.class);
 
         mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
@@ -166,9 +144,6 @@
         mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
         mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
 
-        mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
-        mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
-
         mResizeAnimationDuration = context.getResources().getInteger(
                 R.integer.config_pipResizeAnimationDuration);
         mPipMenuFadeAnimationDuration = context.getResources()
@@ -178,63 +153,18 @@
                 .getDimensionPixelSize(R.dimen.pip_menu_outer_space);
         mPipMenuBorderWidth = context.getResources()
                 .getDimensionPixelSize(R.dimen.pip_menu_border_width);
-        mEduTextHeight = context.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
-        mEduTextFadeExitAnimationDurationMs = context.getResources()
-                .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
-        mEduTextSlideExitAnimationDurationMs = context.getResources()
-                .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
 
-        initEduText();
+        mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener);
+        ((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder))
+                .addView(mEduTextDrawer);
     }
 
-    void initEduText() {
-        final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
-        final SpannableString spannableString = new SpannableString(eduText);
-        Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
-                .ifPresent(annotation -> {
-                    final Drawable icon =
-                            getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
-                    if (icon != null) {
-                        icon.mutate();
-                        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
-                        spannableString.setSpan(new CenteredImageSpan(icon),
-                                eduText.getSpanStart(annotation),
-                                eduText.getSpanEnd(annotation),
-                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    }
-                });
-
-        mEduTextView.setText(spannableString);
-    }
-
-    void setEduTextActive(boolean active) {
-        mEduTextView.setSelected(active);
-    }
-
-    void hideEduText() {
-        final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
-        heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
-        heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
-        heightAnimation.addUpdateListener(animator -> {
-            mEduTextHeight = (int) animator.getAnimatedValue();
-        });
-        mEduTextView.animate()
-                .alpha(0f)
-                .setInterpolator(TvPipInterpolators.EXIT)
-                .setDuration(mEduTextFadeExitAnimationDurationMs)
-                .withEndAction(() -> {
-                    mEduTextContainerView.setVisibility(GONE);
-                }).start();
-        heightAnimation.start();
-    }
-
-    void onPipTransitionStarted(Rect finishBounds) {
+    void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
         // Fade out content by fading in view on top.
-        if (mCurrentPipBounds != null && finishBounds != null) {
+        if (mCurrentPipBounds != null && targetBounds != null) {
             boolean ratioChanged = PipUtils.aspectRatioChanged(
                     mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
-                    finishBounds.width() / (float) finishBounds.height());
+                    targetBounds.width() / (float) targetBounds.height());
             if (ratioChanged) {
                 mPipBackground.animate()
                         .alpha(1f)
@@ -245,11 +175,12 @@
         }
 
         // Update buttons.
-        final boolean vertical = finishBounds.height() > finishBounds.width();
+        final boolean vertical = targetBounds.height() > targetBounds.width();
         final boolean orientationChanged =
                 vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
+                "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
+                TAG, orientationChanged);
         if (!orientationChanged) {
             return;
         }
@@ -261,18 +192,18 @@
                     .setInterpolator(TvPipInterpolators.EXIT)
                     .setDuration(mResizeAnimationDuration / 2)
                     .withEndAction(() -> {
-                        changeButtonScrollOrientation(finishBounds);
-                        updateButtonGravity(finishBounds);
+                        changeButtonScrollOrientation(targetBounds);
+                        updateButtonGravity(targetBounds);
                         // Only make buttons visible again in onPipTransitionFinished to keep in
                         // sync with PiP content alpha animation.
                     });
         } else {
-            changeButtonScrollOrientation(finishBounds);
-            updateButtonGravity(finishBounds);
+            changeButtonScrollOrientation(targetBounds);
+            updateButtonGravity(targetBounds);
         }
     }
 
-    void onPipTransitionFinished(boolean isTvPipExpanded) {
+    void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransitionFinished()", TAG);
 
@@ -283,6 +214,10 @@
                 .setInterpolator(TvPipInterpolators.ENTER)
                 .start();
 
+        if (enterTransition) {
+            mEduTextDrawer.init();
+        }
+
         setIsExpanded(isTvPipExpanded);
 
         // Update buttons.
@@ -409,7 +344,7 @@
     Rect getPipMenuContainerBounds(Rect pipBounds) {
         final Rect menuUiBounds = new Rect(pipBounds);
         menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
-        menuUiBounds.bottom += mEduTextHeight;
+        menuUiBounds.bottom += mEduTextDrawer.getHeight();
         return menuUiBounds;
     }
 
@@ -438,10 +373,6 @@
 
     }
 
-    void setListener(@Nullable Listener listener) {
-        mListener = listener;
-    }
-
     void setExpandedModeEnabled(boolean enabled) {
         mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
     }
@@ -460,21 +391,19 @@
      */
     void showMoveMenu(int gravity) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
-        mButtonMenuIsVisible = false;
-        mMoveMenuIsVisible = true;
         showButtonsMenu(false);
         showMovementHints(gravity);
         setFrameHighlighted(true);
 
         mHorizontalScrollView.setFocusable(false);
         mScrollView.setFocusable(false);
+
+        mEduTextDrawer.closeIfNeeded();
     }
 
     void showButtonsMenu() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showButtonsMenu()", TAG);
-        mButtonMenuIsVisible = true;
-        mMoveMenuIsVisible = false;
         showButtonsMenu(true);
         hideMovementHints();
         setFrameHighlighted(true);
@@ -501,8 +430,6 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: hideAllUserControls()", TAG);
         mFocusedButton = null;
-        mButtonMenuIsVisible = false;
-        mMoveMenuIsVisible = false;
         showButtonsMenu(false);
         hideMovementHints();
         setFrameHighlighted(false);
@@ -632,8 +559,6 @@
 
     @Override
     public void onClick(View v) {
-        if (mListener == null) return;
-
         final int id = v.getId();
         if (id == R.id.tv_pip_menu_fullscreen_button) {
             mListener.onFullscreenButtonClick();
@@ -662,7 +587,7 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
-        if (mListener != null && event.getAction() == ACTION_UP) {
+        if (event.getAction() == ACTION_UP) {
             if (!mMoveMenuIsVisible) {
                 mFocusedButton = mActionButtonsContainer.getFocusedChild();
             }
@@ -700,6 +625,11 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
 
+        if (mMoveMenuIsVisible) {
+            return;
+        }
+        mMoveMenuIsVisible = true;
+
         animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
         animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
         animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
@@ -714,9 +644,7 @@
         animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
         if (a11yEnabled) {
             mA11yDoneButton.setOnClickListener(v -> {
-                if (mListener != null) {
-                    mListener.onExitMoveMode();
-                }
+                mListener.onExitMoveMode();
             });
         }
     }
@@ -725,9 +653,7 @@
         arrowView.setClickable(enabled);
         if (enabled) {
             arrowView.setOnClickListener(v -> {
-                if (mListener != null) {
-                    mListener.onPipMovement(keycode);
-                }
+                mListener.onPipMovement(keycode);
             });
         }
     }
@@ -743,6 +669,11 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: hideMovementHints()", TAG);
 
+        if (!mMoveMenuIsVisible) {
+            return;
+        }
+        mMoveMenuIsVisible = false;
+
         animateAlphaTo(0, mArrowUp);
         animateAlphaTo(0, mArrowRight);
         animateAlphaTo(0, mArrowDown);
@@ -756,19 +687,25 @@
     public void showButtonsMenu(boolean show) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showUserActions: %b", TAG, show);
+        if (mButtonMenuIsVisible == show) {
+            return;
+        }
+        mButtonMenuIsVisible = show;
+
         if (show) {
             mActionButtonsContainer.setVisibility(VISIBLE);
             refocusPreviousButton();
         }
         animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
         animateAlphaTo(show ? 1 : 0, mDimLayer);
+        mEduTextDrawer.closeIfNeeded();
     }
 
     private void setFrameHighlighted(boolean highlighted) {
         mMenuFrameView.setActivated(highlighted);
     }
 
-    interface Listener {
+    interface Listener extends TvPipMenuEduTextDrawer.Listener {
 
         void onBackPress();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d2e8624..fb5a504 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -716,8 +716,8 @@
                         null /* newDisplayAreaInfo */);
             }
         }
-        active.mToken = mOrganizer.startTransition(
-                request.getType(), transitionToken, wct);
+        mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
+        active.mToken = transitionToken;
         mActiveTransitions.add(active);
     }
 
@@ -726,7 +726,7 @@
             @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) {
         final ActiveTransition active = new ActiveTransition();
         active.mHandler = handler;
-        active.mToken = mOrganizer.startTransition(type, null /* token */, wct);
+        active.mToken = mOrganizer.startNewTransition(type, wct);
         mActiveTransitions.add(active);
         return active.mToken;
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index c9c8943..6370df4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -32,14 +32,15 @@
 import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
 import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd
 import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible
+import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import org.junit.Assume
 import org.junit.Test
 
 /**
- * Base test class containing common assertions for [ComponentMatcher.NAV_BAR],
- * [ComponentMatcher.TASK_BAR], [ComponentMatcher.STATUS_BAR], and general assertions (layers
- * visible in consecutive states, entire screen covered, etc.)
+ * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR],
+ * [ComponentNameMatcher.TASK_BAR], [ComponentNameMatcher.STATUS_BAR], and general assertions
+ * (layers visible in consecutive states, entire screen covered, etc.)
  */
 abstract class BaseTest
 @JvmOverloads
@@ -73,9 +74,11 @@
     }
 
     /** Checks that all parts of the screen are covered during the transition */
-    open fun entireScreenCovered() = testSpec.entireScreenCovered()
+    @Presubmit @Test open fun entireScreenCovered() = testSpec.entireScreenCovered()
 
-    /** Checks that the [ComponentMatcher.NAV_BAR] layer is visible during the whole transition */
+    /**
+     * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition
+     */
     @Presubmit
     @Test
     open fun navBarLayerIsVisibleAtStartAndEnd() {
@@ -84,7 +87,8 @@
     }
 
     /**
-     * Checks the position of the [ComponentMatcher.NAV_BAR] at the start and end of the transition
+     * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the
+     * transition
      */
     @Presubmit
     @Test
@@ -94,7 +98,7 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.NAV_BAR] window is visible during the whole transition
+     * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition
      *
      * Note: Phones only
      */
@@ -105,7 +109,9 @@
         testSpec.navBarWindowIsAlwaysVisible()
     }
 
-    /** Checks that the [ComponentMatcher.TASK_BAR] layer is visible during the whole transition */
+    /**
+     * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible during the whole transition
+     */
     @Presubmit
     @Test
     open fun taskBarLayerIsVisibleAtStartAndEnd() {
@@ -114,7 +120,7 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.TASK_BAR] window is visible during the whole transition
+     * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition
      *
      * Note: Large screen only
      */
@@ -126,7 +132,8 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.STATUS_BAR] layer is visible during the whole transition
+     * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible during the whole
+     * transition
      */
     @Presubmit
     @Test
@@ -134,7 +141,7 @@
         testSpec.statusBarLayerIsVisibleAtStartAndEnd()
 
     /**
-     * Checks the position of the [ComponentMatcher.STATUS_BAR] at the start and end of the
+     * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
      * transition
      */
     @Presubmit
@@ -142,7 +149,8 @@
     open fun statusBarLayerPositionAtStartAndEnd() = testSpec.statusBarLayerPositionAtStartAndEnd()
 
     /**
-     * Checks that the [ComponentMatcher.STATUS_BAR] window is visible during the whole transition
+     * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole
+     * transition
      */
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
new file mode 100644
index 0000000..bcd01a4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Postsubmit
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window via pinch out gesture.
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExpandPipOnPinchOpenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+    override val transition: FlickerBuilder.() -> Unit
+        get() = buildTransition {
+            transitions {
+                pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30)
+            }
+        }
+
+    /**
+     * Checks that the visible region area of [pipApp] always increases during the animation.
+     */
+    @Postsubmit
+    @Test
+    fun pipLayerAreaIncreases() {
+        testSpec.assertLayers {
+            val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+            pipLayerList.zipWithNext { previous, current ->
+                previous.visibleRegion.notBiggerThan(current.visibleRegion.region)
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+         * repetitions, screen orientation and navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                .getConfigNonRotationTests(
+                    supportedRotations = listOf(Surface.ROTATION_0)
+                )
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 6292130..2fc0914 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -51,6 +51,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
@@ -93,6 +94,7 @@
     private @Mock Lazy<Transitions> mMockTransitionsLazy;
     private @Mock CompatUIWindowManager mMockCompatLayout;
     private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
+    private @Mock DockStateReader mDockStateReader;
 
     @Captor
     ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -113,7 +115,7 @@
         mShellInit = spy(new ShellInit(mMockExecutor));
         mController = new CompatUIController(mContext, mShellInit, mMockShellController,
                 mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
-                mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) {
+                mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader) {
             @Override
             CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
                     ShellTaskOrganizer.TaskListener taskListener) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index f3a8cf4..16517c0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -54,6 +54,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.transition.Transitions;
 
@@ -103,6 +104,7 @@
     @Mock private SurfaceControlViewHost mViewHost;
     @Mock private Transitions mTransitions;
     @Mock private Runnable mOnDismissCallback;
+    @Mock private DockStateReader mDockStateReader;
 
     private SharedPreferences mSharedPreferences;
     @Nullable
@@ -153,6 +155,16 @@
     }
 
     @Test
+    public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() {
+        LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
+                true, /* isDocked */ true);
+
+        assertFalse(windowManager.createLayout(/* canShow= */ true));
+
+        assertNull(windowManager.mLayout);
+    }
+
+    @Test
     public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
                 true, USER_ID_1, /* isTaskbarEduShowing= */ true);
@@ -382,17 +394,27 @@
         return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false);
     }
 
+    private LetterboxEduWindowManager createWindowManager(boolean eligible, boolean isDocked) {
+        return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */
+                false, isDocked);
+    }
+
     private LetterboxEduWindowManager createWindowManager(boolean eligible,
             int userId, boolean isTaskbarEduShowing) {
+        return createWindowManager(eligible, userId, isTaskbarEduShowing, /* isDocked */false);
+    }
+
+    private LetterboxEduWindowManager createWindowManager(boolean eligible,
+            int userId, boolean isTaskbarEduShowing, boolean isDocked) {
+        doReturn(isDocked).when(mDockStateReader).isDocked();
         LetterboxEduWindowManager windowManager = new LetterboxEduWindowManager(mContext,
                 createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener,
                 createDisplayLayout(), mTransitions, mOnDismissCallback,
-                mAnimationController);
+                mAnimationController, mDockStateReader);
 
         spyOn(windowManager);
         doReturn(mViewHost).when(windowManager).createSurfaceViewHost();
         doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing();
-
         return windowManager;
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index c6492be..db9136d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -45,7 +45,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.clearInvocations;
@@ -67,10 +66,12 @@
 import android.view.WindowManager;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
+import android.window.IWindowContainerToken;
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 import android.window.WindowOrganizer;
 
@@ -117,7 +118,7 @@
     @Before
     public void setUp() {
         doAnswer(invocation -> invocation.getArguments()[1])
-                .when(mOrganizer).startTransition(anyInt(), any(), any());
+                .when(mOrganizer).startTransition(any(), any());
     }
 
     @Test
@@ -136,7 +137,7 @@
         IBinder transitToken = new Binder();
         transitions.requestStartTransition(transitToken,
                 new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
-        verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
+        verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
         TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
                 .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
         transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
@@ -188,7 +189,7 @@
         // Make a request that will be rejected by the testhandler.
         transitions.requestStartTransition(transitToken,
                 new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
-        verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull());
+        verify(mOrganizer, times(1)).startTransition(eq(transitToken), isNull());
         transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
                 mock(SurfaceControl.Transaction.class));
         assertEquals(1, mDefaultHandler.activeCount());
@@ -199,10 +200,12 @@
         // Make a request that will be handled by testhandler but not animated by it.
         RunningTaskInfo mwTaskInfo =
                 createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
+        // Make the wct non-empty.
+        handlerWCT.setFocusable(new WindowContainerToken(mock(IWindowContainerToken.class)), true);
         transitions.requestStartTransition(transitToken,
                 new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */));
         verify(mOrganizer, times(1)).startTransition(
-                eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT));
+                eq(transitToken), eq(handlerWCT));
         transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
                 mock(SurfaceControl.Transaction.class));
         assertEquals(1, mDefaultHandler.activeCount());
@@ -217,8 +220,8 @@
         transitions.addHandler(topHandler);
         transitions.requestStartTransition(transitToken,
                 new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */));
-        verify(mOrganizer, times(1)).startTransition(
-                eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT));
+        verify(mOrganizer, times(2)).startTransition(
+                eq(transitToken), eq(handlerWCT));
         TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE)
                 .addChange(TRANSIT_CHANGE).build();
         transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class),
@@ -256,7 +259,7 @@
         transitions.requestStartTransition(transitToken,
                 new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */,
                         new RemoteTransition(testRemote)));
-        verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
+        verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
         TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
                 .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
         transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
@@ -406,7 +409,7 @@
         IBinder transitToken = new Binder();
         transitions.requestStartTransition(transitToken,
                 new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
-        verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
+        verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
         TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
                 .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
         transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
diff --git a/media/tests/AudioPolicyTest/Android.bp b/media/tests/AudioPolicyTest/Android.bp
index 95d1c6c..fcf91f0 100644
--- a/media/tests/AudioPolicyTest/Android.bp
+++ b/media/tests/AudioPolicyTest/Android.bp
@@ -10,15 +10,11 @@
 android_test {
     name: "audiopolicytest",
     srcs: ["**/*.java"],
-    libs: [
-        "android.test.runner",
-        "android.test.base",
-    ],
     static_libs: [
-        "mockito-target-minus-junit4",
+        "androidx.test.ext.junit",
         "androidx.test.rules",
-        "android-ex-camera2",
-        "testng",
+        "guava",
+        "platform-test-annotations",
     ],
     platform_apis: true,
     certificate: "platform",
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index a7ab828..f696735 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -24,7 +24,7 @@
 
     <application>
         <uses-library android:name="android.test.runner" />
-        <activity android:label="@string/app_name" android:name="AudioPolicyTest"
+        <activity android:label="@string/app_name" android:name="AudioPolicyTestActivity"
                   android:screenOrientation="landscape" android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -33,11 +33,6 @@
         </activity>
     </application>
 
-    <!--instrumentation android:name=".AudioPolicyTestRunner"
-            android:targetPackage="com.android.audiopolicytest"
-            android:label="AudioManager policy oriented integration tests InstrumentationRunner">
-    </instrumentation-->
-
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.audiopolicytest"
             android:label="AudioManager policy oriented integration tests InstrumentationRunner">
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
index 27cf943..94df40d 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
@@ -16,28 +16,57 @@
 
 package com.android.audiopolicytest;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
-import static org.testng.Assert.assertThrows;
+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 android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
+import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import com.google.common.primitives.Ints;
 
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.List;
 
-public class AudioManagerTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioManagerTest {
     private static final String TAG = "AudioManagerTest";
 
+    private AudioManager mAudioManager;
+
+    @Rule
+    public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
+    @Before
+    public void setUp() {
+        mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+    }
+
     //-----------------------------------------------------------------
     // Test getAudioProductStrategies and validate strategies
     //-----------------------------------------------------------------
-    public void testGetAndValidateProductStrategies() throws Exception {
+    @Test
+    public void testGetAndValidateProductStrategies() {
         List<AudioProductStrategy> audioProductStrategies =
                 mAudioManager.getAudioProductStrategies();
         assertTrue(audioProductStrategies.size() > 0);
@@ -101,8 +130,8 @@
     //-----------------------------------------------------------------
     // Test getAudioVolumeGroups and validate volume groups
     //-----------------------------------------------------------------
-
-    public void testGetAndValidateVolumeGroups() throws Exception {
+    @Test
+    public void testGetAndValidateVolumeGroups() {
         List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
         assertTrue(audioVolumeGroups.size() > 0);
 
@@ -118,7 +147,7 @@
             // for each volume group attributes, find the matching product strategy and ensure
             // it is linked the considered volume group
             for (final AudioAttributes aa : avgAttributes) {
-                if (aa.equals(sDefaultAttributes)) {
+                if (aa.equals(DEFAULT_ATTRIBUTES)) {
                     // Some volume groups may not have valid attributes, used for internal
                     // volume management like patch/rerouting
                     // so bailing out strategy retrieval from attributes
@@ -180,6 +209,7 @@
     //-----------------------------------------------------------------
     // Test Volume per Attributes setter/getters
     //-----------------------------------------------------------------
+    @Test
     public void testSetGetVolumePerAttributesWithInvalidAttributes() throws Exception {
         AudioAttributes nullAttributes = null;
 
@@ -197,7 +227,8 @@
                         nullAttributes, 0 /*index*/, 0/*flags*/));
     }
 
-    public void testSetGetVolumePerAttributes() throws Exception {
+    @Test
+    public void testSetGetVolumePerAttributes() {
         for (int usage : AudioAttributes.SDK_USAGES) {
             if (usage == AudioAttributes.USAGE_UNKNOWN) {
                 continue;
@@ -248,12 +279,14 @@
     //-----------------------------------------------------------------
     // Test register/unregister VolumeGroupCallback
     //-----------------------------------------------------------------
-    public void testVolumeGroupCallback() throws Exception {
+    @Test
+    public void testVolumeGroupCallback() {
         List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
         assertTrue(audioVolumeGroups.size() > 0);
 
         AudioVolumeGroupCallbackHelper vgCbReceiver = new AudioVolumeGroupCallbackHelper();
-        mAudioManager.registerVolumeGroupCallback(mContext.getMainExecutor(), vgCbReceiver);
+        mAudioManager.registerVolumeGroupCallback(getApplicationContext().getMainExecutor(),
+                vgCbReceiver);
 
         final List<Integer> publicStreams = Ints.asList(AudioManager.getPublicStreamTypes());
         try {
@@ -273,7 +306,7 @@
 
                 // Set the volume per attributes (if valid) and wait the callback
                 for (final AudioAttributes aa : avgAttributes) {
-                    if (aa.equals(sDefaultAttributes)) {
+                    if (aa.equals(DEFAULT_ATTRIBUTES)) {
                         // Some volume groups may not have valid attributes, used for internal
                         // volume management like patch/rerouting
                         // so bailing out strategy retrieval from attributes
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
similarity index 90%
rename from media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTest.java
rename to media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
index e0c7b22..e31c01a 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
@@ -19,9 +19,9 @@
 import android.app.Activity;
 import android.os.Bundle;
 
-public class AudioPolicyTest extends Activity  {
+public class AudioPolicyTestActivity extends Activity  {
 
-    public AudioPolicyTest() {
+    public AudioPolicyTestActivity() {
     }
 
     /** Called when the activity is first created. */
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java
index 0e918d1..b66545a 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java
@@ -16,25 +16,43 @@
 
 package com.android.audiopolicytest;
 
+import static com.android.audiopolicytest.AudioVolumeTestUtil.INVALID_ATTRIBUTES;
+
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
+import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.List;
 
-public class AudioProductStrategyTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioProductStrategyTest {
     private static final String TAG = "AudioProductStrategyTest";
 
+    @Rule
+    public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
     //-----------------------------------------------------------------
     // Test getAudioProductStrategies and validate strategies
     //-----------------------------------------------------------------
-    public void testGetProductStrategies() throws Exception {
+    @Test
+    public void testGetProductStrategies() {
         List<AudioProductStrategy> audioProductStrategies =
                 AudioProductStrategy.getAudioProductStrategies();
 
@@ -65,6 +83,7 @@
     //-----------------------------------------------------------------
     // Test stream to/from attributes conversion
     //-----------------------------------------------------------------
+    @Test
     public void testAudioAttributesFromStreamTypes() throws Exception {
         List<AudioProductStrategy> audioProductStrategies =
                 AudioProductStrategy.getAudioProductStrategies();
@@ -81,7 +100,7 @@
             // hosting this stream type; Bailing out the test, just ensure that any request
             // for reciproque API with the unknown attributes would return default stream
             // for volume control, aka STREAM_MUSIC.
-            if (aaFromStreamType.equals(sInvalidAttributes)) {
+            if (aaFromStreamType.equals(INVALID_ATTRIBUTES)) {
                 assertEquals(AudioSystem.STREAM_MUSIC,
                         AudioProductStrategy.getLegacyStreamTypeForStrategyWithAudioAttributes(
                             aaFromStreamType));
@@ -139,7 +158,8 @@
         }
     }
 
-    public void testAudioAttributesToStreamTypes() throws Exception {
+    @Test
+    public void testAudioAttributesToStreamTypes() {
         List<AudioProductStrategy> audioProductStrategies =
                 AudioProductStrategy.getAudioProductStrategies();
 
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
index 221f1f7..82394a2 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
@@ -16,21 +16,48 @@
 
 package com.android.audiopolicytest;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
+
 import static org.junit.Assert.assertEquals;
-import static org.testng.Assert.assertThrows;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.audiopolicy.AudioVolumeGroup;
 import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.List;
 
-public class AudioVolumeGroupChangeHandlerTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeGroupChangeHandlerTest {
     private static final String TAG = "AudioVolumeGroupChangeHandlerTest";
 
-    public void testRegisterInvalidCallback() throws Exception {
+    @Rule
+    public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
+    private AudioManager mAudioManager;
+
+    @Before
+    public void setUp() {
+        mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+    }
+
+    @Test
+    public void testRegisterInvalidCallback() {
         final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
                 new AudioVolumeGroupChangeHandler();
 
@@ -42,7 +69,8 @@
         });
     }
 
-    public void testUnregisterInvalidCallback() throws Exception {
+    @Test
+    public void testUnregisterInvalidCallback() {
         final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
                 new AudioVolumeGroupChangeHandler();
 
@@ -58,7 +86,8 @@
         audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
     }
 
-    public void testRegisterUnregisterCallback() throws Exception {
+    @Test
+    public void testRegisterUnregisterCallback() {
         final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
                 new AudioVolumeGroupChangeHandler();
 
@@ -72,7 +101,8 @@
         audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
     }
 
-    public void testCallbackReceived() throws Exception {
+    @Test
+    public void testCallbackReceived() {
         final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
                 new AudioVolumeGroupChangeHandler();
 
@@ -90,7 +120,7 @@
 
                 List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
                 // Set the volume per attributes (if valid) and wait the callback
-                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(sDefaultAttributes)) {
+                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
                     // Some volume groups may not have valid attributes, used for internal
                     // volume management like patch/rerouting
                     // so bailing out strategy retrieval from attributes
@@ -118,7 +148,8 @@
         }
     }
 
-    public void testMultipleCallbackReceived() throws Exception {
+    @Test
+    public void testMultipleCallbackReceived() {
 
         final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
                 new AudioVolumeGroupChangeHandler();
@@ -144,7 +175,7 @@
 
                 List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
                 // Set the volume per attributes (if valid) and wait the callback
-                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(sDefaultAttributes)) {
+                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
                     // Some volume groups may not have valid attributes, used for internal
                     // volume management like patch/rerouting
                     // so bailing out strategy retrieval from attributes
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java
index 84b24b8..1880983 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java
@@ -16,22 +16,51 @@
 
 package com.android.audiopolicytest;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import android.media.AudioAttributes;
+import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.List;
 
-public class AudioVolumeGroupTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeGroupTest {
     private static final String TAG = "AudioVolumeGroupTest";
 
+    @Rule
+    public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
+    private AudioManager mAudioManager;
+
+    @Before
+    public void setUp() {
+        mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+    }
+
     //-----------------------------------------------------------------
     // Test getAudioVolumeGroups and validate groud id
     //-----------------------------------------------------------------
-    public void testGetVolumeGroupsFromNonServiceCaller() throws Exception {
+    @Test
+    public void testGetVolumeGroupsFromNonServiceCaller() {
         // The transaction behind getAudioVolumeGroups will fail. Check is done at binder level
         // with policy service. Error is not reported, the list is just empty.
         // Request must come from service components
@@ -44,7 +73,8 @@
     //-----------------------------------------------------------------
     // Test getAudioVolumeGroups and validate groud id
     //-----------------------------------------------------------------
-    public void testGetVolumeGroups() throws Exception {
+    @Test
+    public void testGetVolumeGroups() {
         // Through AudioManager, the transaction behind getAudioVolumeGroups will succeed
         final List<AudioVolumeGroup> audioVolumeGroup = mAudioManager.getAudioVolumeGroups();
         assertNotNull(audioVolumeGroup);
@@ -67,7 +97,7 @@
             // for each volume group attributes, find the matching product strategy and ensure
             // it is linked the considered volume group
             for (final AudioAttributes aa : avgAttributes) {
-                if (aa.equals(sDefaultAttributes)) {
+                if (aa.equals(DEFAULT_ATTRIBUTES)) {
                     // Some volume groups may not have valid attributes, used for internal
                     // volume management like patch/rerouting
                     // so bailing out strategy retrieval from attributes
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestUtil.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestUtil.java
new file mode 100644
index 0000000..42b249f
--- /dev/null
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestUtil.java
@@ -0,0 +1,37 @@
+/*
+ * 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.audiopolicytest;
+
+import android.media.AudioAttributes;
+import android.media.audiopolicy.AudioProductStrategy;
+
+class AudioVolumeTestUtil {
+    // Default matches the invalid (empty) attributes from native.
+    // The difference is the input source default which is not aligned between native and java
+    public static final AudioAttributes DEFAULT_ATTRIBUTES =
+            AudioProductStrategy.getDefaultAttributes();
+    public static final AudioAttributes INVALID_ATTRIBUTES = new AudioAttributes.Builder().build();
+
+    public static int resetVolumeIndex(int indexMin, int indexMax) {
+        return (indexMax + indexMin) / 2;
+    }
+
+    public static int incrementVolumeIndex(int index, int indexMin, int indexMax) {
+        return (index + 1 > indexMax) ? resetVolumeIndex(indexMin, indexMax) : ++index;
+    }
+
+}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestBase.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
similarity index 74%
rename from media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestBase.java
rename to media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
index b30ef30..fc3b198 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestBase.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
@@ -16,35 +16,36 @@
 
 package com.android.audiopolicytest;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
-import android.test.ActivityInstrumentationTestCase2;
+
+import androidx.test.core.app.ActivityScenario;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.rules.ExternalResource;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-public class AudioVolumesTestBase extends ActivityInstrumentationTestCase2<AudioPolicyTest> {
-    public AudioManager mAudioManager;
-    Context mContext;
+final class AudioVolumesTestRule extends ExternalResource {
+    private AudioManager mAudioManager;
+    private Context mContext;
     private Map<Integer, Integer> mOriginalStreamVolumes = new HashMap<>();
     private Map<Integer, Integer> mOriginalVolumeGroupVolumes = new HashMap<>();
 
-    // Default matches the invalid (empty) attributes from native.
-    // The difference is the input source default which is not aligned between native and java
-    public static final AudioAttributes sDefaultAttributes =
-            AudioProductStrategy.getDefaultAttributes();
-
-    public static final AudioAttributes sInvalidAttributes = new AudioAttributes.Builder().build();
-
-    public AudioVolumesTestBase() {
-        super("com.android.audiopolicytest", AudioPolicyTest.class);
-    }
-
     /**
      * <p>Note: must be called with shell permission (MODIFY_AUDIO_ROUTING)
      */
@@ -56,14 +57,14 @@
                 // like rerouting/patch since these groups are internal to audio policy manager
                 continue;
             }
-            AudioAttributes avgAttributes = sDefaultAttributes;
+            AudioAttributes avgAttributes = DEFAULT_ATTRIBUTES;
             for (final AudioAttributes aa : avg.getAudioAttributes()) {
                 if (!aa.equals(AudioProductStrategy.getDefaultAttributes())) {
                     avgAttributes = aa;
                     break;
                 }
             }
-            if (avgAttributes.equals(sDefaultAttributes)) {
+            if (avgAttributes.equals(DEFAULT_ATTRIBUTES)) {
                 // This shall not happen, however, not purpose of this base class.
                 // so bailing out.
                 continue;
@@ -82,14 +83,14 @@
             for (final AudioVolumeGroup avg : audioVolumeGroups) {
                 if (avg.getId() == e.getKey()) {
                     assertTrue(!avg.getAudioAttributes().isEmpty());
-                    AudioAttributes avgAttributes = sDefaultAttributes;
+                    AudioAttributes avgAttributes = DEFAULT_ATTRIBUTES;
                     for (final AudioAttributes aa : avg.getAudioAttributes()) {
                         if (!aa.equals(AudioProductStrategy.getDefaultAttributes())) {
                             avgAttributes = aa;
                             break;
                         }
                     }
-                    assertTrue(!avgAttributes.equals(sDefaultAttributes));
+                    assertTrue(!avgAttributes.equals(DEFAULT_ATTRIBUTES));
                     mAudioManager.setVolumeIndexForAttributes(
                             avgAttributes, e.getValue(), AudioManager.FLAG_ALLOW_RINGER_MODES);
                 }
@@ -97,11 +98,11 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
+        ActivityScenario.launch(AudioPolicyTestActivity.class);
 
-        mContext = getActivity();
+        mContext = getApplicationContext();
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
 
         assertEquals(PackageManager.PERMISSION_GRANTED,
@@ -117,10 +118,8 @@
         storeAllVolumes();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-
+    @After
+    public void tearDown() throws Exception {
         // Recover the volume and the ringer mode that the test may have overwritten.
         for (Map.Entry<Integer, Integer> e : mOriginalStreamVolumes.entrySet()) {
             mAudioManager.setStreamVolume(e.getKey(), e.getValue(),
@@ -131,11 +130,5 @@
         restoreAllVolumes();
     }
 
-    public static int resetVolumeIndex(int indexMin, int indexMax) {
-        return (indexMax + indexMin) / 2;
-    }
 
-    public static int incrementVolumeIndex(int index, int indexMin, int indexMax) {
-        return (index + 1 > indexMax) ? resetVolumeIndex(indexMin, indexMax) : ++index;
-    }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 60a8e4b..fbec1bc 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -79,10 +79,8 @@
     sheetShape = Shapes.medium,
   ) {}
   LaunchedEffect(state.currentValue) {
-    when (state.currentValue) {
-      ModalBottomSheetValue.Hidden -> {
-        cancelActivity()
-      }
+    if (state.currentValue == ModalBottomSheetValue.Hidden) {
+      cancelActivity()
     }
   }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 4b957e8..1ca70ed 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -64,10 +64,8 @@
     sheetShape = Shapes.medium,
   ) {}
   LaunchedEffect(state.currentValue) {
-    when (state.currentValue) {
-      ModalBottomSheetValue.Hidden -> {
-        cancelActivity()
-      }
+    if (state.currentValue == ModalBottomSheetValue.Hidden) {
+      cancelActivity()
     }
   }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index f501682..1a76943 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -44,8 +44,6 @@
         Settings.System.DIM_SCREEN,
         Settings.System.SCREEN_OFF_TIMEOUT,
         Settings.System.SCREEN_BRIGHTNESS_MODE,
-        Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
-        Settings.System.SCREEN_BRIGHTNESS_FOR_VR,
         Settings.System.ADAPTIVE_SLEEP,             // moved to secure
         Settings.System.APPLY_RAMPING_RINGER,
         Settings.System.VIBRATE_INPUT_DEVICES,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index b1979c9..8f6924c 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -100,7 +100,9 @@
                     Settings.System.MIN_REFRESH_RATE, // depends on hardware capabilities
                     Settings.System.PEAK_REFRESH_RATE, // depends on hardware capabilities
                     Settings.System.SCREEN_BRIGHTNESS_FLOAT,
+                    Settings.System.SCREEN_BRIGHTNESS_FOR_VR,
                     Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT,
+                    Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
                     Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific
                     );
 
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
new file mode 100644
index 0000000..4eb7c7d
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+/** Detects usage of Context.getSystemService() and suggests to use an injected instance instead. */
+@Suppress("UnstableApiUsage")
+class NonInjectedServiceDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf("getSystemService")
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val evaluator = context.evaluator
+        if (
+            !evaluator.isStatic(method) &&
+                method.name == "getSystemService" &&
+                method.containingClass?.qualifiedName == "android.content.Context"
+        ) {
+            context.report(
+                ISSUE,
+                method,
+                context.getNameLocation(node),
+                "Use @Inject to get the handle to a system-level services instead of using " +
+                    "Context.getSystemService()"
+            )
+        }
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "NonInjectedService",
+                briefDescription =
+                    "System-level services should be retrieved using " +
+                        "@Inject instead of Context.getSystemService().",
+                explanation =
+                    "Context.getSystemService() should be avoided because it makes testing " +
+                        "difficult. Instead, use an injected service. For example, " +
+                        "instead of calling Context.getSystemService(UserManager.class), " +
+                        "use @Inject and add UserManager to the constructor",
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(NonInjectedServiceDetector::class.java, Scope.JAVA_FILE_SCOPE)
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
index b72d03d..eb71d32 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
@@ -27,6 +27,7 @@
 import com.intellij.psi.PsiMethod
 import org.jetbrains.uast.UCallExpression
 
+@Suppress("UnstableApiUsage")
 class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner {
 
     override fun getApplicableMethodNames(): List<String> {
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 4879883..312810b 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -35,6 +35,7 @@
                 GetMainLooperViaContextDetector.ISSUE,
                 RegisterReceiverViaContextDetector.ISSUE,
                 SoftwareBitmapDetector.ISSUE,
+                NonInjectedServiceDetector.ISSUE,
         )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
new file mode 100644
index 0000000..26bd8d0
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+
+/*
+ * This file contains stubs of framework APIs and System UI classes for testing purposes only. The
+ * stubs are not used in the lint detectors themselves.
+ */
+@Suppress("UnstableApiUsage")
+internal val androidStubs =
+    arrayOf(
+        java(
+            """
+package android.app;
+
+public class ActivityManager {
+    public static int getCurrentUser() {}
+}
+"""
+        ),
+        java(
+            """
+package android.os;
+import android.content.pm.UserInfo;
+import android.annotation.UserIdInt;
+
+public class UserManager {
+    public UserInfo getUserInfo(@UserIdInt int userId) {}
+}
+"""
+        ),
+        java("""
+package android.annotation;
+
+public @interface UserIdInt {}
+"""),
+        java("""
+package android.content.pm;
+
+public class UserInfo {}
+"""),
+        java("""
+package android.os;
+
+public class Looper {}
+"""),
+        java("""
+package android.os;
+
+public class Handler {}
+"""),
+        java("""
+package android.content;
+
+public class ServiceConnection {}
+"""),
+        java("""
+package android.os;
+
+public enum UserHandle {
+    ALL
+}
+"""),
+        java(
+            """
+package android.content;
+import android.os.UserHandle;
+import android.os.Handler;
+import android.os.Looper;
+import java.util.concurrent.Executor;
+
+public class Context {
+    public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {}
+    public void registerReceiverAsUser(
+            BroadcastReceiver receiver, UserHandle user, IntentFilter filter,
+            String broadcastPermission, Handler scheduler) {}
+    public void registerReceiverForAllUsers(
+            BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission,
+            Handler scheduler) {}
+    public void sendBroadcast(Intent intent) {}
+    public void sendBroadcast(Intent intent, String receiverPermission) {}
+    public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {}
+    public void bindService(Intent intent) {}
+    public void bindServiceAsUser(
+            Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {}
+    public void unbindService(ServiceConnection connection) {}
+    public Looper getMainLooper() { return null; }
+    public Executor getMainExecutor() { return null; }
+    public Handler getMainThreadHandler() { return null; }
+    public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; }
+    public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name);
+}
+"""
+        ),
+        java(
+            """
+package android.app;
+import android.content.Context;
+
+public class Activity extends Context {}
+"""
+        ),
+        java(
+            """
+package android.graphics;
+
+public class Bitmap {
+    public enum Config {
+        ARGB_8888,
+        RGB_565,
+        HARDWARE
+    }
+    public static Bitmap createBitmap(int width, int height, Config config) {
+        return null;
+    }
+}
+"""
+        ),
+        java("""
+package android.content;
+
+public class BroadcastReceiver {}
+"""),
+        java("""
+package android.content;
+
+public class IntentFilter {}
+"""),
+        java(
+            """
+package com.android.systemui.settings;
+import android.content.pm.UserInfo;
+
+public interface UserTracker {
+    int getUserId();
+    UserInfo getUserInfo();
+}
+"""
+        ),
+    )
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
similarity index 61%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
index bf685f7..564afcb 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
@@ -17,26 +17,26 @@
 package com.android.internal.systemui.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
+@Suppress("UnstableApiUsage")
 class BindServiceViaContextDetectorTest : LintDetectorTest() {
 
     override fun getDetector(): Detector = BindServiceViaContextDetector()
     override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
-    override fun getIssues(): List<Issue> = listOf(
-            BindServiceViaContextDetector.ISSUE)
+    override fun getIssues(): List<Issue> = listOf(BindServiceViaContextDetector.ISSUE)
 
     private val explanation = "Binding or unbinding services are synchronous calls"
 
     @Test
     fun testBindService() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -49,17 +49,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(BindServiceViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
     @Test
     fun testBindServiceAsUser() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -73,17 +76,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(BindServiceViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
     @Test
     fun testUnbindService() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -96,45 +102,15 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(BindServiceViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
-    private val contextStub: TestFile = java(
-            """
-        package android.content;
-        import android.os.UserHandle;
-
-        public class Context {
-            public void bindService(Intent intent) {};
-            public void bindServiceAsUser(Intent intent, ServiceConnection connection, int flags,
-                                          UserHandle userHandle) {};
-            public void unbindService(ServiceConnection connection) {};
-        }
-        """
-    )
-
-    private val serviceConnectionStub: TestFile = java(
-            """
-        package android.content;
-
-        public class ServiceConnection {}
-        """
-    )
-
-    private val userHandleStub: TestFile = java(
-            """
-        package android.os;
-
-        public enum UserHandle {
-            ALL
-        }
-        """
-    )
-
-    private val stubs = arrayOf(contextStub, serviceConnectionStub, userHandleStub)
+    private val stubs = androidStubs
 }
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
similarity index 62%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index da010212f2..06aee8e 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -1,26 +1,43 @@
+/*
+ * 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.internal.systemui.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
+@Suppress("UnstableApiUsage")
 class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
 
     override fun getDetector(): Detector = BroadcastSentViaContextDetector()
     override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
-    override fun getIssues(): List<Issue> = listOf(
-        BroadcastSentViaContextDetector.ISSUE)
+    override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
 
     @Test
     fun testSendBroadcast() {
-        lint().files(
-            TestFiles.java(
-                """
+        println(stubs.size)
+        lint()
+            .files(
+                TestFiles.java(
+                        """
                     package test.pkg;
                     import android.content.Context;
 
@@ -31,21 +48,25 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
+                    )
+                    .indented(),
+                *stubs
+            )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
             .expectWarningCount(1)
             .expectContains(
-            "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                    "Context, use com.android.systemui.broadcast.BroadcastSender instead.")
+                "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
+                    "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+            )
     }
 
     @Test
     fun testSendBroadcastAsUser() {
-        lint().files(
-            TestFiles.java(
-                """
+        lint()
+            .files(
+                TestFiles.java(
+                        """
                     package test.pkg;
                     import android.content.Context;
                     import android.os.UserHandle;
@@ -56,21 +77,26 @@
                           context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
                         }
                     }
-                """).indented(),
-                *stubs)
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
             .expectWarningCount(1)
             .expectContains(
-            "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                    "Context, use com.android.systemui.broadcast.BroadcastSender instead.")
+                "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
+                    "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+            )
     }
 
     @Test
     fun testSendBroadcastInActivity() {
-        lint().files(
-            TestFiles.java(
-                """
+        lint()
+            .files(
+                TestFiles.java(
+                        """
                     package test.pkg;
                     import android.app.Activity;
                     import android.os.UserHandle;
@@ -82,21 +108,26 @@
                         }
 
                     }
-                """).indented(),
-                *stubs)
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
             .expectWarningCount(1)
             .expectContains(
-            "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                    "Context, use com.android.systemui.broadcast.BroadcastSender instead.")
+                "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
+                    "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+            )
     }
 
     @Test
     fun testNoopIfNoCall() {
-        lint().files(
-            TestFiles.java(
-                """
+        lint()
+            .files(
+                TestFiles.java(
+                        """
                     package test.pkg;
                     import android.content.Context;
 
@@ -106,45 +137,15 @@
                           context.startActivity(intent);
                         }
                     }
-                """).indented(),
-                *stubs)
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
             .expectClean()
     }
 
-    private val contextStub: TestFile = java(
-        """
-        package android.content;
-        import android.os.UserHandle;
-
-        public class Context {
-            public void sendBroadcast(Intent intent) {};
-            public void sendBroadcast(Intent intent, String receiverPermission) {};
-            public void sendBroadcastAsUser(Intent intent, UserHandle userHandle,
-                                                String permission) {};
-        }
-        """
-    )
-
-    private val activityStub: TestFile = java(
-        """
-        package android.app;
-        import android.content.Context;
-
-        public class Activity extends Context {}
-        """
-    )
-
-    private val userHandleStub: TestFile = java(
-        """
-        package android.os;
-
-        public enum UserHandle {
-            ALL
-        }
-        """
-    )
-
-    private val stubs = arrayOf(contextStub, activityStub, userHandleStub)
+    private val stubs = androidStubs
 }
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
similarity index 64%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
index ec761cd..c55f399 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
@@ -17,13 +17,13 @@
 package com.android.internal.systemui.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
+@Suppress("UnstableApiUsage")
 class GetMainLooperViaContextDetectorTest : LintDetectorTest() {
 
     override fun getDetector(): Detector = GetMainLooperViaContextDetector()
@@ -35,7 +35,8 @@
 
     @Test
     fun testGetMainThreadHandler() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -48,17 +49,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(GetMainLooperViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(GetMainLooperViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
     @Test
     fun testGetMainLooper() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -71,17 +75,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(GetMainLooperViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(GetMainLooperViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
     @Test
     fun testGetMainExecutor() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -94,42 +101,15 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(GetMainLooperViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(GetMainLooperViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
-    private val contextStub: TestFile = java(
-            """
-        package android.content;
-        import android.os.Handler;import android.os.Looper;import java.util.concurrent.Executor;
-
-        public class Context {
-            public Looper getMainLooper() { return null; };
-            public Executor getMainExecutor() { return null; };
-            public Handler getMainThreadHandler() { return null; };
-        }
-        """
-    )
-
-    private val looperStub: TestFile = java(
-            """
-        package android.os;
-
-        public class Looper {}
-        """
-    )
-
-    private val handlerStub: TestFile = java(
-            """
-        package android.os;
-
-        public class Handler {}
-        """
-    )
-
-    private val stubs = arrayOf(contextStub, looperStub, handlerStub)
+    private val stubs = androidStubs
 }
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
new file mode 100644
index 0000000..6b9f88f
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class NonInjectedServiceDetectorTest : LintDetectorTest() {
+
+    override fun getDetector(): Detector = NonInjectedServiceDetector()
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+    override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
+
+    @Test
+    fun testGetServiceWithString() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+                        import android.content.Context;
+
+                        public class TestClass1 {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                context.getSystemService("user");
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(NonInjectedServiceDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains("Use @Inject to get the handle")
+    }
+
+    @Test
+    fun testGetServiceWithClass() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+                        import android.content.Context;
+                        import android.os.UserManager;
+
+                        public class TestClass2 {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                context.getSystemService(UserManager.class);
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(NonInjectedServiceDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains("Use @Inject to get the handle")
+    }
+
+    private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
similarity index 60%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index 76c0519..802ceba 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -17,26 +17,26 @@
 package com.android.internal.systemui.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
+@Suppress("UnstableApiUsage")
 class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
 
     override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
     override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
-    override fun getIssues(): List<Issue> = listOf(
-            RegisterReceiverViaContextDetector.ISSUE)
+    override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
 
     private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher."
 
     @Test
     fun testRegisterReceiver() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -51,17 +51,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(RegisterReceiverViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(RegisterReceiverViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
     @Test
     fun testRegisterReceiverAsUser() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -79,17 +82,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(RegisterReceiverViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(RegisterReceiverViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
     @Test
     fun testRegisterReceiverForAllUsers() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     package test.pkg;
@@ -107,65 +113,15 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(RegisterReceiverViaContextDetector.ISSUE)
-                .run()
-                .expectWarningCount(1)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(RegisterReceiverViaContextDetector.ISSUE)
+            .run()
+            .expectWarningCount(1)
+            .expectContains(explanation)
     }
 
-    private val contextStub: TestFile = java(
-            """
-        package android.content;
-        import android.os.Handler;
-        import android.os.UserHandle;
-
-        public class Context {
-            public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
-                int flags) {};
-            public void registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
-                IntentFilter filter, String broadcastPermission, Handler scheduler) {};
-            public void registerReceiverForAllUsers(BroadcastReceiver receiver, IntentFilter filter,
-                String broadcastPermission, Handler scheduler) {};
-        }
-        """
-    )
-
-    private val broadcastReceiverStub: TestFile = java(
-            """
-        package android.content;
-
-        public class BroadcastReceiver {}
-        """
-    )
-
-    private val intentFilterStub: TestFile = java(
-            """
-        package android.content;
-
-        public class IntentFilter {}
-        """
-    )
-
-    private val handlerStub: TestFile = java(
-            """
-        package android.os;
-
-        public class Handler {}
-        """
-    )
-
-    private val userHandleStub: TestFile = java(
-            """
-        package android.os;
-
-        public enum UserHandle {
-            ALL
-        }
-        """
-    )
-
-    private val stubs = arrayOf(contextStub, broadcastReceiverStub, intentFilterStub, handlerStub,
-            userHandleStub)
+    private val stubs = androidStubs
 }
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
similarity index 74%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index 2738f04..e265837 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -1,13 +1,29 @@
+/*
+ * 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.internal.systemui.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
+@Suppress("UnstableApiUsage")
 class SlowUserQueryDetectorTest : LintDetectorTest() {
 
     override fun getDetector(): Detector = SlowUserQueryDetector()
@@ -134,61 +150,5 @@
             .expectClean()
     }
 
-    private val activityManagerStub: TestFile =
-        java(
-            """
-            package android.app;
-
-            public class ActivityManager {
-                public static int getCurrentUser() {};
-            }
-            """
-        )
-
-    private val userManagerStub: TestFile =
-        java(
-            """
-            package android.os;
-            import android.content.pm.UserInfo;
-            import android.annotation.UserIdInt;
-
-            public class UserManager {
-                public UserInfo getUserInfo(@UserIdInt int userId) {};
-            }
-            """
-        )
-
-    private val userIdIntStub: TestFile =
-        java(
-            """
-            package android.annotation;
-
-            public @interface UserIdInt {}
-            """
-        )
-
-    private val userInfoStub: TestFile =
-        java(
-            """
-            package android.content.pm;
-
-            public class UserInfo {}
-            """
-        )
-
-    private val userTrackerStub: TestFile =
-        java(
-            """
-            package com.android.systemui.settings;
-            import android.content.pm.UserInfo;
-
-            public interface UserTracker {
-                public int getUserId();
-                public UserInfo getUserInfo();
-            }
-            """
-        )
-
-    private val stubs =
-        arrayOf(activityManagerStub, userManagerStub, userIdIntStub, userInfoStub, userTrackerStub)
+    private val stubs = androidStubs
 }
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
similarity index 70%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index 890f2b8..fd6ab09 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -17,7 +17,6 @@
 package com.android.internal.systemui.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
@@ -36,7 +35,8 @@
 
     @Test
     fun testSoftwareBitmap() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     import android.graphics.Bitmap;
@@ -48,17 +48,20 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(SoftwareBitmapDetector.ISSUE)
-                .run()
-                .expectWarningCount(2)
-                .expectContains(explanation)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(SoftwareBitmapDetector.ISSUE)
+            .run()
+            .expectWarningCount(2)
+            .expectContains(explanation)
     }
 
     @Test
     fun testHardwareBitmap() {
-        lint().files(
+        lint()
+            .files(
                 TestFiles.java(
                         """
                     import android.graphics.Bitmap;
@@ -69,29 +72,14 @@
                         }
                     }
                 """
-                ).indented(),
-                *stubs)
-                .issues(SoftwareBitmapDetector.ISSUE)
-                .run()
-                .expectWarningCount(0)
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(SoftwareBitmapDetector.ISSUE)
+            .run()
+            .expectWarningCount(0)
     }
 
-    private val bitmapStub: TestFile = java(
-            """
-        package android.graphics;
-
-        public class Bitmap {
-            public enum Config {
-                ARGB_8888,
-                RGB_565,
-                HARDWARE
-            }
-            public static Bitmap createBitmap(int width, int height, Config config) {
-                return null;
-            }
-        }
-        """
-    )
-
-    private val stubs = arrayOf(bitmapStub)
+    private val stubs = androidStubs
 }
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
index 1ce106e..716c4fe 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
@@ -61,7 +61,7 @@
         </com.android.systemui.statusbar.phone.MultiUserSwitch>
 
         <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-            android:id="@+id/settings_button_container"
+            android:id="@id/settings_button_container"
             android:layout_width="@dimen/qs_footer_action_button_size"
             android:layout_height="@dimen/qs_footer_action_button_size"
             android:background="@drawable/qs_footer_action_circle"
@@ -85,7 +85,7 @@
         </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
 
         <com.android.systemui.statusbar.AlphaOptimizedImageView
-            android:id="@+id/pm_lite"
+            android:id="@id/pm_lite"
             android:layout_width="@dimen/qs_footer_action_button_size"
             android:layout_height="@dimen/qs_footer_action_button_size"
             android:background="@drawable/qs_footer_action_circle_color"
diff --git a/packages/SystemUI/res-keyguard/values/ids.xml b/packages/SystemUI/res-keyguard/values/ids.xml
new file mode 100644
index 0000000..0dff4ff
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  ~
+  -->
+
+<resources>
+    <item type="id" name="header_footer_views_added_tag_key" />
+</resources>
diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml
index d886806..ae8e38e 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip.xml
@@ -16,7 +16,7 @@
 <!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view
      is the root view of a window, we cannot change the root view's margins.) -->
 <!-- Alphas start as 0 because the view will be animated in. -->
-<FrameLayout
+<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/media_ttt_sender_chip"
@@ -97,4 +97,4 @@
             />
 
     </LinearLayout>
-</FrameLayout>
+</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index f22e797..ba5f67f 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -188,5 +188,10 @@
     <item type="id" name="face_scanning_anim"/>
 
     <item type="id" name="qqs_tile_layout"/>
+
+    <!-- The buttons in the Quick Settings footer actions.-->
+    <item type="id" name="settings_button_container"/>
+    <item type="id" name="pm_lite"/>
+
 </resources>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index e77c650..2b2b05ce 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -81,11 +81,6 @@
      */
     void stopScreenPinning() = 17;
 
-    /*
-     * Notifies that the swipe-to-home (recents animation) is finished.
-     */
-    void notifySwipeToHomeFinished() = 23;
-
     /**
      * Notifies that quickstep will switch to a new task
      * @param rotation indicates which Surface.Rotation the gesture was started in
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
new file mode 100644
index 0000000..72f8b7b
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -0,0 +1,209 @@
+package com.android.systemui.shared.recents.utilities;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.Surface;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * Utility class to position the thumbnail in the TaskView
+ */
+public class PreviewPositionHelper {
+
+    public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
+
+    // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
+    private final RectF mClippedInsets = new RectF();
+    private final Matrix mMatrix = new Matrix();
+    private boolean mIsOrientationChanged;
+
+    public Matrix getMatrix() {
+        return mMatrix;
+    }
+
+    public void setOrientationChanged(boolean orientationChanged) {
+        mIsOrientationChanged = orientationChanged;
+    }
+
+    public boolean isOrientationChanged() {
+        return mIsOrientationChanged;
+    }
+
+    /**
+     * Updates the matrix based on the provided parameters
+     */
+    public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
+            int canvasWidth, int canvasHeight, int screenWidthPx, int taskbarSize, boolean isTablet,
+            int currentRotation, boolean isRtl) {
+        boolean isRotated = false;
+        boolean isOrientationDifferent;
+
+        int thumbnailRotation = thumbnailData.rotation;
+        int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+        RectF thumbnailClipHint = new RectF();
+        float canvasScreenRatio = canvasWidth / (float) screenWidthPx;
+        float scaledTaskbarSize = taskbarSize * canvasScreenRatio;
+        thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
+
+        float scale = thumbnailData.scale;
+        final float thumbnailScale;
+
+        // Landscape vs portrait change.
+        // Note: Disable rotation in grid layout.
+        boolean windowingModeSupportsRotation =
+                thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN && !isTablet;
+        isOrientationDifferent = isOrientationChange(deltaRotate)
+                && windowingModeSupportsRotation;
+        if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
+            // If we haven't measured , skip the thumbnail drawing and only draw the background
+            // color
+            thumbnailScale = 0f;
+        } else {
+            // Rotate the screenshot if not in multi-window mode
+            isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
+
+            float surfaceWidth = thumbnailBounds.width() / scale;
+            float surfaceHeight = thumbnailBounds.height() / scale;
+            float availableWidth = surfaceWidth
+                    - (thumbnailClipHint.left + thumbnailClipHint.right);
+            float availableHeight = surfaceHeight
+                    - (thumbnailClipHint.top + thumbnailClipHint.bottom);
+
+            float canvasAspect = canvasWidth / (float) canvasHeight;
+            float availableAspect = isRotated
+                    ? availableHeight / availableWidth
+                    : availableWidth / availableHeight;
+            boolean isAspectLargelyDifferent =
+                    Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect,
+                            availableAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
+            if (isRotated && isAspectLargelyDifferent) {
+                // Do not rotate thumbnail if it would not improve fit
+                isRotated = false;
+                isOrientationDifferent = false;
+            }
+
+            if (isAspectLargelyDifferent) {
+                // Crop letterbox insets if insets isn't already clipped
+                thumbnailClipHint.left = thumbnailData.letterboxInsets.left;
+                thumbnailClipHint.right = thumbnailData.letterboxInsets.right;
+                thumbnailClipHint.top = thumbnailData.letterboxInsets.top;
+                thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom;
+                availableWidth = surfaceWidth
+                        - (thumbnailClipHint.left + thumbnailClipHint.right);
+                availableHeight = surfaceHeight
+                        - (thumbnailClipHint.top + thumbnailClipHint.bottom);
+            }
+
+            final float targetW, targetH;
+            if (isOrientationDifferent) {
+                targetW = canvasHeight;
+                targetH = canvasWidth;
+            } else {
+                targetW = canvasWidth;
+                targetH = canvasHeight;
+            }
+            float targetAspect = targetW / targetH;
+
+            // Update the clipHint such that
+            //   > the final clipped position has same aspect ratio as requested by canvas
+            //   > first fit the width and crop the extra height
+            //   > if that will leave empty space, fit the height and crop the width instead
+            float croppedWidth = availableWidth;
+            float croppedHeight = croppedWidth / targetAspect;
+            if (croppedHeight > availableHeight) {
+                croppedHeight = availableHeight;
+                if (croppedHeight < targetH) {
+                    croppedHeight = Math.min(targetH, surfaceHeight);
+                }
+                croppedWidth = croppedHeight * targetAspect;
+
+                // One last check in case the task aspect radio messed up something
+                if (croppedWidth > surfaceWidth) {
+                    croppedWidth = surfaceWidth;
+                    croppedHeight = croppedWidth / targetAspect;
+                }
+            }
+
+            // Update the clip hints. Align to 0,0, crop the remaining.
+            if (isRtl) {
+                thumbnailClipHint.left += availableWidth - croppedWidth;
+                if (thumbnailClipHint.right < 0) {
+                    thumbnailClipHint.left += thumbnailClipHint.right;
+                    thumbnailClipHint.right = 0;
+                }
+            } else {
+                thumbnailClipHint.right += availableWidth - croppedWidth;
+                if (thumbnailClipHint.left < 0) {
+                    thumbnailClipHint.right += thumbnailClipHint.left;
+                    thumbnailClipHint.left = 0;
+                }
+            }
+            thumbnailClipHint.bottom += availableHeight - croppedHeight;
+            if (thumbnailClipHint.top < 0) {
+                thumbnailClipHint.bottom += thumbnailClipHint.top;
+                thumbnailClipHint.top = 0;
+            } else if (thumbnailClipHint.bottom < 0) {
+                thumbnailClipHint.top += thumbnailClipHint.bottom;
+                thumbnailClipHint.bottom = 0;
+            }
+
+            thumbnailScale = targetW / (croppedWidth * scale);
+        }
+
+        if (!isRotated) {
+            mMatrix.setTranslate(
+                    -thumbnailClipHint.left * scale,
+                    -thumbnailClipHint.top * scale);
+        } else {
+            setThumbnailRotation(deltaRotate, thumbnailBounds);
+        }
+
+        mClippedInsets.set(0, 0, 0, scaledTaskbarSize);
+
+        mMatrix.postScale(thumbnailScale, thumbnailScale);
+        mIsOrientationChanged = isOrientationDifferent;
+    }
+
+    private int getRotationDelta(int oldRotation, int newRotation) {
+        int delta = newRotation - oldRotation;
+        if (delta < 0) delta += 4;
+        return delta;
+    }
+
+    /**
+     * @param deltaRotation the number of 90 degree turns from the current orientation
+     * @return {@code true} if the change in rotation results in a shift from landscape to
+     * portrait or vice versa, {@code false} otherwise
+     */
+    private boolean isOrientationChange(int deltaRotation) {
+        return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+    }
+
+    private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
+        float translateX = 0;
+        float translateY = 0;
+
+        mMatrix.setRotate(90 * deltaRotate);
+        switch (deltaRotate) { /* Counter-clockwise */
+            case Surface.ROTATION_90:
+                translateX = thumbnailPosition.height();
+                break;
+            case Surface.ROTATION_270:
+                translateY = thumbnailPosition.width();
+                break;
+            case Surface.ROTATION_180:
+                translateX = thumbnailPosition.width();
+                translateY = thumbnailPosition.height();
+                break;
+        }
+        mMatrix.postTranslate(translateX, translateY);
+    }
+
+    public RectF getClippedInsets() {
+        return mClippedInsets;
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
index 56326e3..77a13bd 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
@@ -62,6 +62,16 @@
         return false; // Default
     }
 
+    /**
+     * Compares the ratio of two quantities and returns whether that ratio is greater than the
+     * provided bound. Order of quantities does not matter. Bound should be a decimal representation
+     * of a percentage.
+     */
+    public static boolean isRelativePercentDifferenceGreaterThan(float first, float second,
+            float bound) {
+        return (Math.abs(first - second) / Math.abs((first + second) / 2.0f)) > bound;
+    }
+
     /** Calculates the constrast between two colors, using the algorithm provided by the WCAG v2. */
     public static float computeContrastBetweenColors(int bg, int fg) {
         float bgR = Color.red(bg) / 255f;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index ca19b78..744e7da 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -69,8 +69,7 @@
 
 import android.annotation.AnyThread;
 import android.annotation.MainThread;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.ActivityTaskManager.RootTaskInfo;
@@ -113,7 +112,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.ServiceState;
@@ -126,6 +124,9 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.InstanceId;
@@ -266,9 +267,9 @@
     private final KeyguardUpdateMonitorLogger mLogger;
     private final boolean mIsPrimaryUser;
     private final AuthController mAuthController;
-    private final StatusBarStateController mStatusBarStateController;
     private final UiEventLogger mUiEventLogger;
     private final Set<Integer> mFaceAcquiredInfoIgnoreList;
+    private final PackageManager mPackageManager;
     private int mStatusBarState;
     private final StatusBarStateController.StateListener mStatusBarStateControllerListener =
             new StatusBarStateController.StateListener() {
@@ -289,7 +290,7 @@
     };
 
     HashMap<Integer, SimData> mSimDatas = new HashMap<>();
-    HashMap<Integer, ServiceState> mServiceStates = new HashMap<Integer, ServiceState>();
+    HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
 
     private int mPhoneState;
     private boolean mKeyguardIsVisible;
@@ -321,34 +322,41 @@
     private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
             mCallbacks = Lists.newArrayList();
     private ContentObserver mDeviceProvisionedObserver;
-    private ContentObserver mTimeFormatChangeObserver;
+    private final ContentObserver mTimeFormatChangeObserver;
 
     private boolean mSwitchingUser;
 
     private boolean mDeviceInteractive;
-    private SubscriptionManager mSubscriptionManager;
+    private final SubscriptionManager mSubscriptionManager;
     private final TelephonyListenerManager mTelephonyListenerManager;
-    private List<SubscriptionInfo> mSubscriptionInfo;
-    private TrustManager mTrustManager;
-    private UserManager mUserManager;
-    private KeyguardBypassController mKeyguardBypassController;
-    private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
-    private int mFaceRunningState = BIOMETRIC_STATE_STOPPED;
-    private LockPatternUtils mLockPatternUtils;
-    private final IDreamManager mDreamManager;
-    private boolean mIsDreaming;
+    private final TrustManager mTrustManager;
+    private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final LatencyTracker mLatencyTracker;
+    private final StatusBarStateController mStatusBarStateController;
+    private final Executor mBackgroundExecutor;
+    private final SensorPrivacyManager mSensorPrivacyManager;
+    private final ActiveUnlockConfig mActiveUnlockConfig;
+    private final PowerManager mPowerManager;
+    private final IDreamManager mDreamManager;
+    private final TelephonyManager mTelephonyManager;
+    @Nullable
+    private final FingerprintManager mFpm;
+    @Nullable
+    private final FaceManager mFaceManager;
+    private final LockPatternUtils mLockPatternUtils;
+    private final boolean mWakeOnFingerprintAcquiredStart;
+
+    private KeyguardBypassController mKeyguardBypassController;
+    private List<SubscriptionInfo> mSubscriptionInfo;
+    private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
+    private int mFaceRunningState = BIOMETRIC_STATE_STOPPED;
+    private boolean mIsDreaming;
     private boolean mLogoutEnabled;
     private boolean mIsFaceEnrolled;
     private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-    private final Executor mBackgroundExecutor;
-    private SensorPrivacyManager mSensorPrivacyManager;
-    private final ActiveUnlockConfig mActiveUnlockConfig;
-    private final PowerManager mPowerManager;
-    private final boolean mWakeOnFingerprintAcquiredStart;
 
     /**
      * Short delay before restarting fingerprint authentication after a successful try. This should
@@ -375,12 +383,10 @@
     }
     private final Handler mHandler;
 
-    private SparseBooleanArray mBiometricEnabledForUser = new SparseBooleanArray();
-    private BiometricManager mBiometricManager;
-    private IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback =
+    private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback =
             new IBiometricEnabledOnKeyguardCallback.Stub() {
                 @Override
-                public void onChanged(boolean enabled, int userId) throws RemoteException {
+                public void onChanged(boolean enabled, int userId) {
                     mHandler.post(() -> {
                         mBiometricEnabledForUser.put(userId, enabled);
                         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
@@ -399,7 +405,7 @@
         }
     };
 
-    private OnSubscriptionsChangedListener mSubscriptionListener =
+    private final OnSubscriptionsChangedListener mSubscriptionListener =
             new OnSubscriptionsChangedListener() {
                 @Override
                 public void onSubscriptionsChanged() {
@@ -418,11 +424,12 @@
         }
     }
 
-    private SparseBooleanArray mUserIsUnlocked = new SparseBooleanArray();
-    private SparseBooleanArray mUserHasTrust = new SparseBooleanArray();
-    private SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray();
-    private SparseBooleanArray mUserTrustIsUsuallyManaged = new SparseBooleanArray();
-    private Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<Integer, Intent>();
+    private final SparseBooleanArray mUserIsUnlocked = new SparseBooleanArray();
+    private final SparseBooleanArray mUserHasTrust = new SparseBooleanArray();
+    private final SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray();
+    private final SparseBooleanArray mUserTrustIsUsuallyManaged = new SparseBooleanArray();
+    private final SparseBooleanArray mBiometricEnabledForUser = new SparseBooleanArray();
+    private final Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<>();
 
     @VisibleForTesting
     SparseArray<BiometricAuthenticated> mUserFingerprintAuthenticated = new SparseArray<>();
@@ -583,7 +590,7 @@
         }
         if (sil == null) {
             // getCompleteActiveSubscriptionInfoList was null callers expect an empty list.
-            mSubscriptionInfo = new ArrayList<SubscriptionInfo>();
+            mSubscriptionInfo = new ArrayList<>();
         } else {
             mSubscriptionInfo = sil;
         }
@@ -718,7 +725,7 @@
      * If the device is dreaming, awakens the device
      */
     public void awakenFromDream() {
-        if (mIsDreaming && mDreamManager != null) {
+        if (mIsDreaming) {
             try {
                 mDreamManager.awaken();
             } catch (RemoteException e) {
@@ -763,12 +770,8 @@
     }
 
     private void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) {
-        mBackgroundExecutor.execute(new Runnable() {
-            @Override
-            public void run() {
-                mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId);
-            }
-        });
+        mBackgroundExecutor.execute(
+                () -> mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId));
     }
 
     private void handleFingerprintAuthFailed() {
@@ -851,7 +854,8 @@
         }
     }
 
-    private Runnable mRetryFingerprintAuthentication = new Runnable() {
+    private final Runnable mRetryFingerprintAuthentication = new Runnable() {
+        @SuppressLint("MissingPermission")
         @Override
         public void run() {
             mLogger.logRetryAfterFpHwUnavailable(mHardwareFingerprintUnavailableRetryCount);
@@ -895,7 +899,7 @@
 
         boolean lockedOutStateChanged = false;
         if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) {
-            lockedOutStateChanged |= !mFingerprintLockedOutPermanent;
+            lockedOutStateChanged = !mFingerprintLockedOutPermanent;
             mFingerprintLockedOutPermanent = true;
             mLogger.d("Fingerprint locked out - requiring strong auth");
             mLockPatternUtils.requireStrongAuth(
@@ -940,9 +944,9 @@
             // that the events will arrive in a particular order. Add a delay here in case
             // an unlock is in progress. In this is a normal unlock the extra delay won't
             // be noticeable.
-            mHandler.postDelayed(() -> {
-                updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
-            }, getBiometricLockoutDelay());
+            mHandler.postDelayed(
+                    () -> updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE),
+                    getBiometricLockoutDelay());
         } else {
             updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         }
@@ -1076,7 +1080,7 @@
         }
     }
 
-    private Runnable mRetryFaceAuthentication = new Runnable() {
+    private final Runnable mRetryFaceAuthentication = new Runnable() {
         @Override
         public void run() {
             mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount);
@@ -1102,12 +1106,8 @@
 
         // Error is always the end of authentication lifecycle
         mFaceCancelSignal = null;
-        boolean cameraPrivacyEnabled = false;
-        if (mSensorPrivacyManager != null) {
-            cameraPrivacyEnabled = mSensorPrivacyManager
-                    .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
-                    SensorPrivacyManager.Sensors.CAMERA);
-        }
+        boolean cameraPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
+                SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, SensorPrivacyManager.Sensors.CAMERA);
 
         if (msgId == FaceManager.FACE_ERROR_CANCELED
                 && mFaceRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING) {
@@ -1158,10 +1158,8 @@
         mFaceLockedOutPermanent = (mode == BIOMETRIC_LOCKOUT_PERMANENT);
         final boolean changed = (mFaceLockedOutPermanent != wasLockoutPermanent);
 
-        mHandler.postDelayed(() -> {
-            updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET);
-        }, getBiometricLockoutDelay());
+        mHandler.postDelayed(() -> updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+                FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET), getBiometricLockoutDelay());
 
         if (changed) {
             notifyLockedOutStateChanged(BiometricSourceType.FACE);
@@ -1200,28 +1198,24 @@
         return mFaceRunningState == BIOMETRIC_STATE_RUNNING;
     }
 
-    private boolean isTrustDisabled(int userId) {
+    private boolean isTrustDisabled() {
         // Don't allow trust agent if device is secured with a SIM PIN. This is here
         // mainly because there's no other way to prompt the user to enter their SIM PIN
         // once they get past the keyguard screen.
-        final boolean disabledBySimPin = isSimPinSecure();
-        return disabledBySimPin;
+        return isSimPinSecure(); // Disabled by SIM PIN
     }
 
     private boolean isFingerprintDisabled(int userId) {
-        final DevicePolicyManager dpm =
-                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
-        return dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId)
-                & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0
+        return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, userId)
+                        & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0
                 || isSimPinSecure();
     }
 
     private boolean isFaceDisabled(int userId) {
-        final DevicePolicyManager dpm =
-                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
         // TODO(b/140035044)
-        return whitelistIpcs(() -> dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId)
-                & DevicePolicyManager.KEYGUARD_DISABLE_FACE) != 0
+        return whitelistIpcs(() ->
+                (mDevicePolicyManager.getKeyguardDisabledFeatures(null, userId)
+                        & DevicePolicyManager.KEYGUARD_DISABLE_FACE) != 0
                 || isSimPinSecure());
     }
 
@@ -1243,7 +1237,7 @@
     }
 
     public boolean getUserHasTrust(int userId) {
-        return !isTrustDisabled(userId) && mUserHasTrust.get(userId);
+        return !isTrustDisabled() && mUserHasTrust.get(userId);
     }
 
     /**
@@ -1275,7 +1269,7 @@
     }
 
     public boolean getUserTrustIsManaged(int userId) {
-        return mUserTrustIsManaged.get(userId) && !isTrustDisabled(userId);
+        return mUserTrustIsManaged.get(userId) && !isTrustDisabled();
     }
 
     private void updateSecondaryLockscreenRequirement(int userId) {
@@ -1293,7 +1287,7 @@
                 Intent intent =
                         new Intent(DevicePolicyManager.ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE)
                                 .setPackage(supervisorComponent.getPackageName());
-                ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 0);
+                ResolveInfo resolveInfo = mPackageManager.resolveService(intent, 0);
                 if (resolveInfo != null && resolveInfo.serviceInfo != null) {
                     Intent launchIntent =
                             new Intent().setComponent(resolveInfo.serviceInfo.getComponentName());
@@ -1662,22 +1656,19 @@
     CancellationSignal mFingerprintCancelSignal;
     @VisibleForTesting
     CancellationSignal mFaceCancelSignal;
-    private FingerprintManager mFpm;
-    private FaceManager mFaceManager;
     private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
     private List<FaceSensorPropertiesInternal> mFaceSensorProperties;
     private boolean mFingerprintLockedOut;
     private boolean mFingerprintLockedOutPermanent;
     private boolean mFaceLockedOutPermanent;
-    private HashMap<Integer, Boolean> mIsUnlockWithFingerprintPossible = new HashMap<>();
-    private TelephonyManager mTelephonyManager;
+    private final HashMap<Integer, Boolean> mIsUnlockWithFingerprintPossible = new HashMap<>();
 
     /**
      * When we receive a
      * {@link com.android.internal.telephony.TelephonyIntents#ACTION_SIM_STATE_CHANGED} broadcast,
      * and then pass a result via our handler to {@link KeyguardUpdateMonitor#handleSimStateChange},
      * we need a single object to pass to the handler.  This class helps decode
-     * the intent and provide a {@link SimCard.State} result.
+     * the intent and provide a {@link SimData} result.
      */
     private static class SimData {
         public int simState;
@@ -1898,9 +1889,20 @@
             UiEventLogger uiEventLogger,
             // This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :(
             Provider<SessionTracker> sessionTrackerProvider,
-            PowerManager powerManager) {
+            PowerManager powerManager,
+            TrustManager trustManager,
+            SubscriptionManager subscriptionManager,
+            UserManager userManager,
+            IDreamManager dreamManager,
+            DevicePolicyManager devicePolicyManager,
+            SensorPrivacyManager sensorPrivacyManager,
+            TelephonyManager telephonyManager,
+            PackageManager packageManager,
+            @Nullable FaceManager faceManager,
+            @Nullable FingerprintManager fingerprintManager,
+            @Nullable BiometricManager biometricManager) {
         mContext = context;
-        mSubscriptionManager = SubscriptionManager.from(context);
+        mSubscriptionManager = subscriptionManager;
         mTelephonyListenerManager = telephonyListenerManager;
         mDeviceProvisioned = isDeviceProvisionedInSettingsDb();
         mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged);
@@ -1914,12 +1916,20 @@
         mLockPatternUtils = lockPatternUtils;
         mAuthController = authController;
         dumpManager.registerDumpable(getClass().getName(), this);
-        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+        mSensorPrivacyManager = sensorPrivacyManager;
         mActiveUnlockConfig = activeUnlockConfiguration;
         mLogger = logger;
         mUiEventLogger = uiEventLogger;
         mSessionTrackerProvider = sessionTrackerProvider;
         mPowerManager = powerManager;
+        mTrustManager = trustManager;
+        mUserManager = userManager;
+        mDreamManager = dreamManager;
+        mTelephonyManager = telephonyManager;
+        mDevicePolicyManager = devicePolicyManager;
+        mPackageManager = packageManager;
+        mFpm = fingerprintManager;
+        mFaceManager = faceManager;
         mActiveUnlockConfig.setKeyguardUpdateMonitor(this);
         mWakeOnFingerprintAcquiredStart = context.getResources()
                         .getBoolean(com.android.internal.R.bool.kg_wake_on_acquire_start);
@@ -2064,8 +2074,7 @@
         // listener now with the service state from the default sub.
         mBackgroundExecutor.execute(() -> {
             int subId = SubscriptionManager.getDefaultSubscriptionId();
-            ServiceState serviceState = mContext.getSystemService(TelephonyManager.class)
-                    .getServiceStateForSubscriber(subId);
+            ServiceState serviceState = mTelephonyManager.getServiceStateForSubscriber(subId);
             mHandler.sendMessage(
                     mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState));
         });
@@ -2087,26 +2096,21 @@
             e.rethrowAsRuntimeException();
         }
 
-        mTrustManager = context.getSystemService(TrustManager.class);
         mTrustManager.registerTrustListener(this);
 
         setStrongAuthTracker(mStrongAuthTracker);
 
-        mDreamManager = IDreamManager.Stub.asInterface(
-                ServiceManager.getService(DreamService.DREAM_SERVICE));
-
-        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
-            mFpm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
+        if (mFpm != null) {
             mFingerprintSensorProperties = mFpm.getSensorPropertiesInternal();
+            mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback);
         }
-        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) {
-            mFaceManager = (FaceManager) context.getSystemService(Context.FACE_SERVICE);
+        if (mFaceManager != null) {
             mFaceSensorProperties = mFaceManager.getSensorPropertiesInternal();
+            mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback);
         }
 
-        if (mFpm != null || mFaceManager != null) {
-            mBiometricManager = context.getSystemService(BiometricManager.class);
-            mBiometricManager.registerEnabledOnKeyguardCallback(mBiometricEnabledCallback);
+        if (biometricManager != null) {
+            biometricManager.registerEnabledOnKeyguardCallback(mBiometricEnabledCallback);
         }
 
         // in case authenticators aren't registered yet at this point:
@@ -2125,19 +2129,11 @@
             }
         });
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT);
-        if (mFpm != null) {
-            mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback);
-        }
-        if (mFaceManager != null) {
-            mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback);
-        }
 
         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
-        mUserManager = context.getSystemService(UserManager.class);
         mIsPrimaryUser = mUserManager.isPrimaryUser();
         int user = ActivityManager.getCurrentUser();
         mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user));
-        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
         mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled();
         updateSecondaryLockscreenRequirement(user);
         List<UserInfo> allUsers = mUserManager.getUsers();
@@ -2147,22 +2143,8 @@
         }
         updateAirplaneModeState();
 
-        mTelephonyManager =
-                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        if (mTelephonyManager != null) {
-            mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
-            // Set initial sim states values.
-            for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) {
-                int state = mTelephonyManager.getSimState(slot);
-                int[] subIds = mSubscriptionManager.getSubscriptionIds(slot);
-                if (subIds != null) {
-                    for (int subId : subIds) {
-                        mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state)
-                                .sendToTarget();
-                    }
-                }
-            }
-        }
+        mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
+        initializeSimState();
 
         mTimeFormatChangeObserver = new ContentObserver(mHandler) {
             @Override
@@ -2179,11 +2161,25 @@
                 false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
     }
 
+    private void initializeSimState() {
+        // Set initial sim states values.
+        for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) {
+            int state = mTelephonyManager.getSimState(slot);
+            int[] subIds = mSubscriptionManager.getSubscriptionIds(slot);
+            if (subIds != null) {
+                for (int subId : subIds) {
+                    mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state)
+                            .sendToTarget();
+                }
+            }
+        }
+    }
+
     private void updateFaceEnrolled(int userId) {
         mIsFaceEnrolled = whitelistIpcs(
                 () -> mFaceManager != null && mFaceManager.isHardwareDetected()
-                        && mBiometricEnabledForUser.get(userId))
-                && mAuthController.isFaceAuthEnrolled(userId);
+                        && mFaceManager.hasEnrolledTemplates(userId)
+                        && mBiometricEnabledForUser.get(userId));
     }
 
     public boolean isFaceSupported() {
@@ -2221,7 +2217,7 @@
         }
 
         @Override
-        public void onUserSwitchComplete(int newUserId) throws RemoteException {
+        public void onUserSwitchComplete(int newUserId) {
             mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCH_COMPLETE,
                     newUserId, 0));
         }
@@ -2780,6 +2776,7 @@
         return isUnlockWithFacePossible(userId) || isUnlockWithFingerprintPossible(userId);
     }
 
+    @SuppressLint("MissingPermission")
     @VisibleForTesting
     boolean isUnlockWithFingerprintPossible(int userId) {
         // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once.
@@ -2910,6 +2907,7 @@
         try {
             reply.sendResult(null);
         } catch (RemoteException e) {
+            mLogger.logException(e, "Ignored exception while userSwitching");
         }
     }
 
@@ -3176,7 +3174,7 @@
             return false;
         }
         Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
-        ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(homeIntent,
+        ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(homeIntent,
                 0 /* flags */, getCurrentUser());
 
         if (resolveInfo == null) {
@@ -3306,11 +3304,7 @@
         }
 
         // change in battery overheat
-        if (current.health != old.health) {
-            return true;
-        }
-
-        return false;
+        return current.health != old.health;
     }
 
     /**
@@ -3361,10 +3355,8 @@
     public void setSwitchingUser(boolean switching) {
         mSwitchingUser = switching;
         // Since this comes in on a binder thread, we need to post if first
-        mHandler.post(() -> {
-            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FACE_AUTH_UPDATED_USER_SWITCHING);
-        });
+        mHandler.post(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
+                FACE_AUTH_UPDATED_USER_SWITCHING));
     }
 
     private void sendUpdates(KeyguardUpdateMonitorCallback callback) {
@@ -3473,7 +3465,7 @@
     /**
      * If any SIM cards are currently secure.
      *
-     * @see #isSimPinSecure(State)
+     * @see #isSimPinSecure(int)
      */
     public boolean isSimPinSecure() {
         // True if any SIM is pin secure
@@ -3520,10 +3512,7 @@
      * @return true if and only if the state has changed for the specified {@code slotId}
      */
     private boolean refreshSimState(int subId, int slotId) {
-        final TelephonyManager tele =
-                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-        int state = (tele != null) ?
-                tele.getSimState(slotId) : TelephonyManager.SIM_STATE_UNKNOWN;
+        int state = mTelephonyManager.getSimState(slotId);
         SimData data = mSimDatas.get(subId);
         final boolean changed;
         if (data == null) {
@@ -3666,13 +3655,8 @@
      * Unregister all listeners.
      */
     public void destroy() {
-        // TODO: inject these dependencies:
-        TelephonyManager telephony =
-                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephony != null) {
-            mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
-        }
-
+        mStatusBarStateController.removeCallback(mStatusBarStateControllerListener);
+        mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
         mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener);
 
         if (mDeviceProvisionedObserver != null) {
@@ -3702,8 +3686,9 @@
         mHandler.removeCallbacksAndMessages(null);
     }
 
+    @SuppressLint("MissingPermission")
     @Override
-    public void dump(PrintWriter pw, String[] args) {
+    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("KeyguardUpdateMonitor state:");
         pw.println("  getUserHasTrust()=" + getUserHasTrust(getCurrentUser()));
         pw.println("  getUserUnlockedWithBiometric()="
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
index efa5558..b793fd2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
@@ -66,10 +66,13 @@
         listView.setDividerHeight(mContext.getResources().getDimensionPixelSize(
                 R.dimen.bouncer_user_switcher_popup_divider_height));
 
-        int height  = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bouncer_user_switcher_popup_header_height);
-        listView.addHeaderView(createSpacer(height), null, false);
-        listView.addFooterView(createSpacer(height), null, false);
+        if (listView.getTag(R.id.header_footer_views_added_tag_key) == null) {
+            int height = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.bouncer_user_switcher_popup_header_height);
+            listView.addHeaderView(createSpacer(height), null, false);
+            listView.addFooterView(createSpacer(height), null, false);
+            listView.setTag(R.id.header_footer_views_added_tag_key, new Object());
+        }
 
         listView.setOnTouchListener((v, ev) -> {
             if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
new file mode 100644
index 0000000..80b9c4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -0,0 +1,53 @@
+package com.android.keyguard.logging
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.MessageInitializer
+import com.android.systemui.log.MessagePrinter
+import com.android.systemui.log.dagger.KeyguardLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "KeyguardLog"
+
+class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) {
+    fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+    fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+    fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+
+    fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+    fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg)
+
+    private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) {
+        buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
+    }
+
+    // TODO: remove after b/237743330 is fixed
+    fun logStatusBarCalculatedAlpha(alpha: Float) {
+        debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
+    }
+
+    // TODO: remove after b/237743330 is fixed
+    fun logStatusBarExplicitAlpha(alpha: Float) {
+        debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" })
+    }
+
+    // TODO: remove after b/237743330 is fixed
+    fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) {
+        debugLog(
+            {
+                int1 = visibility
+                double1 = alpha.toDouble()
+                str1 = state
+            },
+            { "changing visibility to $int1 with alpha $double1 in state: $str1" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 7a00cd9..bf9f4c8 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -45,7 +45,7 @@
 
     fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
 
-    fun v(@CompileTimeConstant msg: String) = log(msg, ERROR)
+    fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
 
     fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index ef3c34a..1ceb6b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -151,7 +151,6 @@
     @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
 
     @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
-    @NonNull private final SparseBooleanArray mFaceEnrolledForUser;
     @NonNull private final SensorPrivacyManager mSensorPrivacyManager;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private boolean mAllFingerprintAuthenticatorsRegistered;
@@ -346,15 +345,6 @@
                 }
             }
         }
-        if (mFaceProps == null) {
-            Log.d(TAG, "handleEnrollmentsChanged, mFaceProps is null");
-        } else {
-            for (FaceSensorPropertiesInternal prop : mFaceProps) {
-                if (prop.sensorId == sensorId) {
-                    mFaceEnrolledForUser.put(userId, hasEnrollments);
-                }
-            }
-        }
         for (Callback cb : mCallbacks) {
             cb.onEnrollmentsChanged(modality);
         }
@@ -719,7 +709,6 @@
         mWindowManager = windowManager;
         mInteractionJankMonitor = jankMonitor;
         mUdfpsEnrolledForUser = new SparseBooleanArray();
-        mFaceEnrolledForUser = new SparseBooleanArray();
         mVibratorHelper = vibrator;
 
         mOrientationListener = new BiometricDisplayListener(
@@ -1068,7 +1057,7 @@
             return false;
         }
 
-        return mFaceEnrolledForUser.get(userId);
+        return mFaceManager.hasEnrolledTemplates(userId);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index c292296..701df89 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -45,6 +45,7 @@
     public static final int QS_SWIPE_SIDE = 15;
     public static final int BACK_GESTURE = 16;
     public static final int QS_SWIPE_NESTED = 17;
+    public static final int MEDIA_SEEKBAR = 18;
 
     @IntDef({
             QUICK_SETTINGS,
@@ -65,7 +66,8 @@
             LOCK_ICON,
             QS_SWIPE_SIDE,
             QS_SWIPE_NESTED,
-            BACK_GESTURE
+            BACK_GESTURE,
+            MEDIA_SEEKBAR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface InteractionType {}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
index 5e4f149..f8ee49a 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
@@ -23,6 +23,7 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_FLING_THRESHOLD_IN;
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_SWIPE_THRESHOLD_IN;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
 import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
@@ -153,6 +154,7 @@
             @Classifier.InteractionType int interactionType,
             double historyBelief, double historyConfidence) {
         if (interactionType == BRIGHTNESS_SLIDER
+                || interactionType == MEDIA_SEEKBAR
                 || interactionType == SHADE_DRAG
                 || interactionType == QS_COLLAPSE
                 || interactionType == Classifier.UDFPS_AUTHENTICATION
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
index 07f94e7..e8c83b1 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_PROXIMITY_PERCENT_COVERED_THRESHOLD;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
@@ -119,7 +120,8 @@
             @Classifier.InteractionType int interactionType,
             double historyBelief, double historyConfidence) {
         if (interactionType == QUICK_SETTINGS || interactionType == BRIGHTNESS_SLIDER
-                || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE) {
+                || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE
+                || interactionType == MEDIA_SEEKBAR) {
             return Result.passed(0);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
index 776bc88..f576a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
 import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
 import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
@@ -93,6 +94,10 @@
             case QS_SWIPE_NESTED:
                 wrongDirection = !vertical;
                 break;
+            case MEDIA_SEEKBAR:
+                confidence = 0;
+                wrongDirection = vertical;
+                break;
             default:
                 wrongDirection = true;
                 break;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
index de2bdf7..840982c 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
@@ -22,6 +22,7 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_ZIGZAG_Y_SECONDARY_DEVIANCE;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
 import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
 
 import android.graphics.Point;
@@ -91,6 +92,7 @@
             @Classifier.InteractionType int interactionType,
             double historyBelief, double historyConfidence) {
         if (interactionType == BRIGHTNESS_SLIDER
+                || interactionType == MEDIA_SEEKBAR
                 || interactionType == SHADE_DRAG
                 || interactionType == LOCK_ICON) {
             return Result.passed(0);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 4096ed4..139a8b7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -46,6 +46,7 @@
 import android.content.res.Resources;
 import android.hardware.SensorManager;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.biometrics.BiometricManager;
 import android.hardware.camera2.CameraManager;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.AmbientDisplayConfiguration;
@@ -237,22 +238,39 @@
     @Singleton
     static IDreamManager provideIDreamManager() {
         return IDreamManager.Stub.asInterface(
-                ServiceManager.checkService(DreamService.DREAM_SERVICE));
+                ServiceManager.getService(DreamService.DREAM_SERVICE));
     }
 
     @Provides
     @Singleton
     @Nullable
     static FaceManager provideFaceManager(Context context) {
-        return context.getSystemService(FaceManager.class);
-
+        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) {
+            return context.getSystemService(FaceManager.class);
+        }
+        return null;
     }
 
     @Provides
     @Singleton
     @Nullable
     static FingerprintManager providesFingerprintManager(Context context) {
-        return context.getSystemService(FingerprintManager.class);
+        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+            return context.getSystemService(FingerprintManager.class);
+        }
+        return null;
+    }
+
+    /**
+     * @return null if both faceManager and fingerprintManager are null.
+     */
+    @Provides
+    @Singleton
+    @Nullable
+    static BiometricManager providesBiometricManager(Context context,
+            @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager) {
+        return faceManager == null && fingerprintManager == null ? null :
+                context.getSystemService(BiometricManager.class);
     }
 
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 443d277..06dbab9 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -81,6 +81,7 @@
 import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
 import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
 import com.android.systemui.statusbar.window.StatusBarWindowModule;
+import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
 import com.android.systemui.tuner.dagger.TunerModule;
 import com.android.systemui.unfold.SysUIUnfoldModule;
 import com.android.systemui.user.UserModule;
@@ -145,6 +146,7 @@
             StatusBarWindowModule.class,
             SysUIConcurrencyModule.class,
             SysUIUnfoldModule.class,
+            TelephonyRepositoryModule.class,
             TunerModule.class,
             UserModule.class,
             UtilModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index b598554..4c4aa5c 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -33,6 +33,18 @@
     boolean isProvisioned();
 
     /**
+     * Whether there's a pulse that's been requested but hasn't started transitioning to pulsing
+     * states yet.
+     */
+    boolean isPulsePending();
+
+    /**
+     * @param isPulsePending whether a pulse has been requested but hasn't started transitioning
+     *                       to the pulse state yet
+     */
+    void setPulsePending(boolean isPulsePending);
+
+    /**
      * Makes a current pulse last for twice as long.
      * @param reason why we're extending it.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 4161cf6..8ae305b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -280,8 +280,8 @@
     /**
      * Appends pulse dropped event to logs
      */
-    public void tracePulseDropped(boolean pulsePending, DozeMachine.State state, boolean blocked) {
-        mLogger.logPulseDropped(pulsePending, state, blocked);
+    public void tracePulseDropped(String from, DozeMachine.State state) {
+        mLogger.logPulseDropped(from, state);
     }
 
     /**
@@ -292,6 +292,13 @@
     }
 
     /**
+     * Appends pulsing event to logs.
+     */
+    public void tracePulseEvent(String pulseEvent, boolean dozing, int pulseReason) {
+        mLogger.logPulseEvent(pulseEvent, dozing, DozeLog.reasonToString(pulseReason));
+    }
+
+    /**
      * Appends pulse dropped event to logs
      * @param reason why the pulse was dropped
      */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 4b279ec..21a2c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -224,13 +224,12 @@
         })
     }
 
-    fun logPulseDropped(pulsePending: Boolean, state: DozeMachine.State, blocked: Boolean) {
+    fun logPulseDropped(from: String, state: DozeMachine.State) {
         buffer.log(TAG, INFO, {
-            bool1 = pulsePending
-            str1 = state.name
-            bool2 = blocked
+            str1 = from
+            str2 = state.name
         }, {
-            "Pulse dropped, pulsePending=$bool1 state=$str1 blocked=$bool2"
+            "Pulse dropped, cannot pulse from=$str1 state=$str2"
         })
     }
 
@@ -243,6 +242,16 @@
         })
     }
 
+    fun logPulseEvent(pulseEvent: String, dozing: Boolean, pulseReason: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = pulseEvent
+            bool1 = dozing
+            str2 = pulseReason
+        }, {
+            "Pulse-$str1 dozing=$bool1 pulseReason=$str2"
+        })
+    }
+
     fun logPulseDropped(reason: String) {
         buffer.log(TAG, INFO, {
             str1 = reason
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 00ac8bc..ef454ff 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -102,7 +102,6 @@
     private final UiEventLogger mUiEventLogger;
 
     private long mNotificationPulseTime;
-    private boolean mPulsePending;
     private Runnable mAodInterruptRunnable;
 
     /** see {@link #onProximityFar} prox for callback */
@@ -303,8 +302,8 @@
                         null /* onPulseSuppressedListener */);
             }
         } else {
-            proximityCheckThenCall((result) -> {
-                if (result != null && result) {
+            proximityCheckThenCall((isNear) -> {
+                if (isNear != null && isNear) {
                     // In pocket, drop event.
                     mDozeLog.traceSensorEventDropped(pulseReason, "prox reporting near");
                     return;
@@ -410,8 +409,8 @@
         sWakeDisplaySensorState = wake;
 
         if (wake) {
-            proximityCheckThenCall((result) -> {
-                if (result != null && result) {
+            proximityCheckThenCall((isNear) -> {
+                if (isNear != null && isNear) {
                     // In pocket, drop event.
                     return;
                 }
@@ -537,24 +536,44 @@
             return;
         }
 
-        if (mPulsePending || !mAllowPulseTriggers || !canPulse()) {
-            if (mAllowPulseTriggers) {
-                mDozeLog.tracePulseDropped(mPulsePending, dozeState, mDozeHost.isPulsingBlocked());
+        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) {
+            if (!mAllowPulseTriggers) {
+                mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers");
+            } else if (mDozeHost.isPulsePending()) {
+                mDozeLog.tracePulseDropped("requestPulse - pulsePending");
+            } else if (!canPulse()) {
+                mDozeLog.tracePulseDropped("requestPulse", dozeState);
             }
             runIfNotNull(onPulseSuppressedListener);
             return;
         }
 
-        mPulsePending = true;
-        proximityCheckThenCall((result) -> {
-            if (result != null && result) {
+        mDozeHost.setPulsePending(true);
+        proximityCheckThenCall((isNear) -> {
+            if (isNear != null && isNear) {
                 // in pocket, abort pulse
-                mDozeLog.tracePulseDropped("inPocket");
-                mPulsePending = false;
+                mDozeLog.tracePulseDropped("requestPulse - inPocket");
+                mDozeHost.setPulsePending(false);
                 runIfNotNull(onPulseSuppressedListener);
             } else {
                 // not in pocket, continue pulsing
-                continuePulseRequest(reason);
+                final boolean isPulsePending = mDozeHost.isPulsePending();
+                mDozeHost.setPulsePending(false);
+                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) {
+                    if (!isPulsePending) {
+                        mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer"
+                                + " pending, pulse was cancelled before it could start"
+                                + " transitioning to pulsing state.");
+                    } else if (mDozeHost.isPulsingBlocked()) {
+                        mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked");
+                    } else if (!canPulse()) {
+                        mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState());
+                    }
+                    runIfNotNull(onPulseSuppressedListener);
+                    return;
+                }
+
+                mMachine.requestPulse(reason);
             }
         }, !mDozeParameters.getProxCheckBeforePulse() || performedProxCheck, reason);
 
@@ -569,16 +588,6 @@
                 || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED;
     }
 
-    private void continuePulseRequest(int reason) {
-        mPulsePending = false;
-        if (mDozeHost.isPulsingBlocked() || !canPulse()) {
-            mDozeLog.tracePulseDropped(mPulsePending, mMachine.getState(),
-                    mDozeHost.isPulsingBlocked());
-            return;
-        }
-        mMachine.requestPulse(reason);
-    }
-
     @Nullable
     private InstanceId getKeyguardSessionId() {
         return mSessionTracker.getSessionId(SESSION_KEYGUARD);
@@ -591,7 +600,7 @@
         pw.print(" notificationPulseTime=");
         pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime));
 
-        pw.println(" pulsePending=" + mPulsePending);
+        pw.println(" DozeHost#isPulsePending=" + mDozeHost.isPulsePending());
         pw.println("DozeSensors:");
         IndentingPrintWriter idpw = new IndentingPrintWriter(pw);
         idpw.increaseIndent();
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 38d9d021..23c96e1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -68,6 +68,9 @@
 
     public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
 
+    public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
+            false);
+
     // next id: 112
 
     /***************************************/
@@ -104,9 +107,26 @@
     public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
             new ReleasedFlag(209, true);
 
-    /** Whether the new implementation of UserSwitcherController should be used. */
-    public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER =
-            new UnreleasedFlag(210, false);
+    /**
+     * Whether the user interactor and repository should use `UserSwitcherController`.
+     *
+     * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
+     * the framework APIs.
+     */
+    public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
+            new UnreleasedFlag(210, true);
+
+    /**
+     * Whether `UserSwitcherController` should use the user interactor.
+     *
+     * <p>When this is {@code true}, the controller does not directly access framework APIs.
+     * Instead, it goes through the interactor.
+     *
+     * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
+     * {@code true} as it would created a cycle between controller -> interactor -> controller.
+     */
+    public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR =
+            new UnreleasedFlag(211, false);
 
     /***************************************/
     // 300 - power menu
@@ -152,7 +172,7 @@
     public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
             new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
 
-    public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507, true);
+    public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507);
 
     /***************************************/
     // 600- status bar
@@ -247,6 +267,13 @@
     public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
             new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
 
+    @Keep
+    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
+            new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
+    @Keep
+    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
+            new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
+
     // 1200 - predictive back
     @Keep
     public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
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 840a4b2..4c4b588 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
@@ -85,6 +85,15 @@
      */
     val dozeAmount: Flow<Float>
 
+    /**
+     * Returns `true` if the keyguard is showing; `false` otherwise.
+     *
+     * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
+     * the z-order (which is not really above the system UI window, but rather - the lock-screen
+     * becomes invisible to reveal the "occluding activity").
+     */
+    fun isKeyguardShowing(): Boolean
+
     /** Sets whether the bottom area UI should animate the transition out of doze state. */
     fun setAnimateDozingTransitions(animate: Boolean)
 
@@ -103,7 +112,7 @@
 @Inject
 constructor(
     statusBarStateController: StatusBarStateController,
-    keyguardStateController: KeyguardStateController,
+    private val keyguardStateController: KeyguardStateController,
     dozeHost: DozeHost,
 ) : KeyguardRepository {
     private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
@@ -168,6 +177,10 @@
         awaitClose { statusBarStateController.removeCallback(callback) }
     }
 
+    override fun isKeyguardShowing(): Boolean {
+        return keyguardStateController.isShowing
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.value = animate
     }
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 dccc941..192919e 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
@@ -29,7 +29,7 @@
 class KeyguardInteractor
 @Inject
 constructor(
-    repository: KeyguardRepository,
+    private val repository: KeyguardRepository,
 ) {
     /**
      * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -40,4 +40,8 @@
     val isDozing: Flow<Boolean> = repository.isDozing
     /** Whether the keyguard is showing ot not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+
+    fun isKeyguardShowing(): Boolean {
+        return repository.isKeyguardShowing()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
new file mode 100644
index 0000000..aef3471
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
@@ -0,0 +1,10 @@
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * A [com.android.systemui.log.LogBuffer] for keyguard-related stuff. Should be used mostly for
+ * adding temporary logs or logging from smaller classes when creating new separate log class might
+ * be an overkill.
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f2f6bad..0c5564b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -43,7 +43,7 @@
     @SysUISingleton
     @DozeLog
     public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) {
-        return factory.create("DozeLog", 100);
+        return factory.create("DozeLog", 120);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -344,4 +344,14 @@
     public static LogBuffer provideUdfpsLogBuffer(LogBufferFactory factory) {
         return factory.create("UdfpsLog", 1000);
     }
+
+    /**
+     * Provides a {@link LogBuffer} for general keyguard-related logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardLog
+    public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) {
+        return factory.create("KeyguardLog", 250);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
index 0359c63..17ebfec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
@@ -30,7 +30,10 @@
 import androidx.core.view.GestureDetectorCompat
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
+import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
 import com.android.systemui.statusbar.NotificationMediaManager
 import com.android.systemui.util.concurrency.RepeatableExecutor
 import javax.inject.Inject
@@ -72,7 +75,8 @@
 
 /** ViewModel for seek bar in QS media player. */
 class SeekBarViewModel @Inject constructor(
-    @Background private val bgExecutor: RepeatableExecutor
+    @Background private val bgExecutor: RepeatableExecutor,
+    private val falsingManager: FalsingManager,
 ) {
     private var _data = Progress(false, false, false, false, null, 0)
         set(value) {
@@ -275,7 +279,7 @@
     /** Gets a listener to attach to the seek bar to handle seeking. */
     val seekBarListener: SeekBar.OnSeekBarChangeListener
         get() {
-            return SeekBarChangeListener(this)
+            return SeekBarChangeListener(this, falsingManager)
         }
 
     /** Attach touch handlers to the seek bar view. */
@@ -315,7 +319,8 @@
     }
 
     private class SeekBarChangeListener(
-        val viewModel: SeekBarViewModel
+        val viewModel: SeekBarViewModel,
+        val falsingManager: FalsingManager,
     ) : SeekBar.OnSeekBarChangeListener {
         override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
             if (fromUser) {
@@ -328,6 +333,13 @@
         }
 
         override fun onStopTrackingTouch(bar: SeekBar) {
+            // in addition to the normal functionality of both functions.
+            // isFalseTouch returns true if there is a real/false tap since it is not a move.
+            // isFalseTap returns true if there is a real/false move since it is not a tap.
+            if (falsingManager.isFalseTouch(MEDIA_SEEKBAR) &&
+                    falsingManager.isFalseTap(LOW_PENALTY)) {
+                viewModel.onSeekFalse()
+            }
             viewModel.onSeek(bar.progress.toLong())
         }
     }
@@ -340,7 +352,7 @@
      */
     private class SeekBarTouchListener(
         private val viewModel: SeekBarViewModel,
-        private val bar: SeekBar
+        private val bar: SeekBar,
     ) : View.OnTouchListener, GestureDetector.OnGestureListener {
 
         // Gesture detector helps decide which touch events to intercept.
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 4379d25..aae973d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -25,6 +25,7 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
 
 /**
@@ -107,12 +108,15 @@
             controllerSender: MediaTttChipControllerSender,
             routeInfo: MediaRoute2Info,
             undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger
+            uiEventLogger: MediaTttSenderUiEventLogger,
+            falsingManager: FalsingManager,
         ): View.OnClickListener? {
             if (undoCallback == null) {
                 return null
             }
             return View.OnClickListener {
+                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
                 uiEventLogger.logUndoClicked(
                     MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
                 )
@@ -143,12 +147,15 @@
             controllerSender: MediaTttChipControllerSender,
             routeInfo: MediaRoute2Info,
             undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger
+            uiEventLogger: MediaTttSenderUiEventLogger,
+            falsingManager: FalsingManager,
         ): View.OnClickListener? {
             if (undoCallback == null) {
                 return null
             }
             return View.OnClickListener {
+                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
                 uiEventLogger.logUndoClicked(
                     MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
                 )
@@ -215,7 +222,8 @@
         controllerSender: MediaTttChipControllerSender,
         routeInfo: MediaRoute2Info,
         undoCallback: IUndoMediaTransferCallback?,
-        uiEventLogger: MediaTttSenderUiEventLogger
+        uiEventLogger: MediaTttSenderUiEventLogger,
+        falsingManager: FalsingManager,
     ): View.OnClickListener? = null
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index e539f3f..007eb8f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -22,25 +22,30 @@
 import android.os.PowerManager
 import android.util.Log
 import android.view.Gravity
+import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
 import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
 import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.Lazy
 import javax.inject.Inject
 
 /**
@@ -57,7 +62,11 @@
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
-        private val uiEventLogger: MediaTttSenderUiEventLogger
+        private val uiEventLogger: MediaTttSenderUiEventLogger,
+        // Added Lazy<> to delay the time we create Falsing instances.
+        // And overcome performance issue, check [b/247817628] for details.
+        private val falsingManager: Lazy<FalsingManager>,
+        private val falsingCollector: Lazy<FalsingCollector>,
 ) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
         context,
         logger,
@@ -70,6 +79,9 @@
         MediaTttUtils.WINDOW_TITLE,
         MediaTttUtils.WAKE_REASON,
 ) {
+
+    private lateinit var parent: MediaTttChipRootView
+
     override val windowLayoutParams = commonWindowLayoutParams.apply {
         gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
     }
@@ -120,6 +132,15 @@
 
         val chipState = newInfo.state
 
+        // Detect falsing touches on the chip.
+        parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
+        parent.touchHandler = object : Gefingerpoken {
+            override fun onTouchEvent(ev: MotionEvent?): Boolean {
+                falsingCollector.get().onTouchEvent(ev)
+                return false
+            }
+        }
+
         // App icon
         val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
             context, newInfo.routeInfo.clientPackageName, logger
@@ -142,7 +163,11 @@
         // Undo
         val undoView = currentView.requireViewById<View>(R.id.undo)
         val undoClickListener = chipState.undoClickListener(
-                this, newInfo.routeInfo, newInfo.undoCallback, uiEventLogger
+                this,
+                newInfo.routeInfo,
+                newInfo.undoCallback,
+                uiEventLogger,
+                falsingManager.get(),
         )
         undoView.setOnClickListener(undoClickListener)
         undoView.visibility = (undoClickListener != null).visibleIfTrue()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
new file mode 100644
index 0000000..3373159
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.taptotransfer.sender
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.widget.FrameLayout
+import com.android.systemui.Gefingerpoken
+
+/** A simple subclass that allows for observing touch events on chip. */
+class MediaTttChipRootView(
+        context: Context,
+        attrs: AttributeSet?
+) : FrameLayout(context, attrs) {
+
+    /** Assign this field to observe touch events. */
+    var touchHandler: Gefingerpoken? = null
+
+    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        touchHandler?.onTouchEvent(ev)
+        return super.dispatchTouchEvent(ev)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index ba3c919..7a44058 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -60,6 +60,7 @@
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -82,7 +83,7 @@
     private static final String EXTRA_VISIBLE = "visible";
 
     private final Rect mQsBounds = new Rect();
-    private final StatusBarStateController mStatusBarStateController;
+    private final SysuiStatusBarStateController mStatusBarStateController;
     private final FalsingManager mFalsingManager;
     private final KeyguardBypassController mBypassController;
     private boolean mQsExpanded;
@@ -159,7 +160,7 @@
      * Progress of pull down from the center of the lock screen.
      * @see com.android.systemui.statusbar.LockscreenShadeTransitionController
      */
-    private float mFullShadeProgress;
+    private float mLockscreenToShadeProgress;
 
     private boolean mOverScrolling;
 
@@ -177,7 +178,7 @@
     @Inject
     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
             QSTileHost qsTileHost,
-            StatusBarStateController statusBarStateController, CommandQueue commandQueue,
+            SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
             @Named(QS_PANEL) MediaHost qsMediaHost,
             @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
             KeyguardBypassController keyguardBypassController,
@@ -585,7 +586,7 @@
             mTransitioningToFullShade = isTransitioningToFullShade;
             updateShowCollapsedOnKeyguard();
         }
-        mFullShadeProgress = qsTransitionFraction;
+        mLockscreenToShadeProgress = qsTransitionFraction;
         setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation,
                 isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction);
     }
@@ -709,10 +710,13 @@
         }
         if (mInSplitShade) {
             // Large screens in landscape.
-            if (mTransitioningToFullShade || isKeyguardState()) {
+            // Need to check upcoming state as for unlocked -> AOD transition current state is
+            // not updated yet, but we're transitioning and UI should already follow KEYGUARD state
+            if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState()
+                    == StatusBarState.KEYGUARD) {
                 // Always use "mFullShadeProgress" on keyguard, because
                 // "panelExpansionFractions" is always 1 on keyguard split shade.
-                return mFullShadeProgress;
+                return mLockscreenToShadeProgress;
             } else {
                 return panelExpansionFraction;
             }
@@ -721,7 +725,7 @@
         if (mTransitioningToFullShade) {
             // Only use this value during the standard lock screen shade expansion. During the
             // "quick" expansion from top, this value is 0.
-            return mFullShadeProgress;
+            return mLockscreenToShadeProgress;
         } else {
             return panelExpansionFraction;
         }
@@ -929,7 +933,7 @@
         indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation);
         indentingPw.println("mInSplitShade: " + mInSplitShade);
         indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade);
-        indentingPw.println("mFullShadeProgress: " + mFullShadeProgress);
+        indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress);
         indentingPw.println("mOverScrolling: " + mOverScrolling);
         indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing());
         View view = getView();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 28ddead..dd1ffcc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -222,6 +222,7 @@
 
     private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
         val buttonView = button.view
+        buttonView.id = model?.id ?: View.NO_ID
         buttonView.isVisible = model != null
         if (model == null) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 2ad0513..5a8f684 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -25,6 +25,7 @@
  * power buttons.
  */
 data class FooterActionsButtonViewModel(
+    val id: Int?,
     val icon: Icon,
     val iconTint: Int?,
     @DrawableRes val background: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index a935338..8b3f4b4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -138,6 +138,7 @@
     /** The model for the settings button. */
     val settings: FooterActionsButtonViewModel =
         FooterActionsButtonViewModel(
+            id = R.id.settings_button_container,
             Icon.Resource(
                 R.drawable.ic_settings,
                 ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
@@ -151,6 +152,7 @@
     val power: FooterActionsButtonViewModel? =
         if (showPowerButton) {
             FooterActionsButtonViewModel(
+                id = R.id.pm_lite,
                 Icon.Resource(
                     android.R.drawable.ic_lock_power_off,
                     ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
@@ -256,6 +258,7 @@
             }
 
         return FooterActionsButtonViewModel(
+            id = null,
             Icon.Loaded(
                 icon,
                 ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 97476b2..d2d5063 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -134,7 +134,7 @@
                 v.bind(name, drawable, item.info.id);
             }
             v.setActivated(item.isCurrent);
-            v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
+            v.setDisabledByAdmin(item.isDisabledByAdmin());
             v.setEnabled(item.isSwitchToEnabled);
             UserSwitcherController.setSelectableAlpha(v);
 
@@ -173,16 +173,16 @@
             Trace.beginSection("UserDetailView.Adapter#onClick");
             UserRecord userRecord =
                     (UserRecord) view.getTag();
-            if (mController.isDisabledByAdmin(userRecord)) {
+            if (userRecord.isDisabledByAdmin()) {
                 final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
-                        mContext, mController.getEnforcedAdmin(userRecord));
+                        mContext, userRecord.enforcedAdmin);
                 mController.startActivity(intent);
             } else if (userRecord.isSwitchToEnabled) {
                 MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER);
                 mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
                 if (!userRecord.isAddUser
                         && !userRecord.isRestricted
-                        && !mController.isDisabledByAdmin(userRecord)) {
+                        && !userRecord.isDisabledByAdmin()) {
                     if (mCurrentUserView != null) {
                         mCurrentUserView.setActivated(false);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 7e2a5c5..899e57d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -342,14 +342,6 @@
         }
 
         @Override
-        public void notifySwipeToHomeFinished() {
-            verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () ->
-                    mPipOptional.ifPresent(
-                            pip -> pip.setPinnedStackAnimationType(
-                                    PipAnimationController.ANIM_TYPE_ALPHA)));
-        }
-
-        @Override
         public void notifySwipeUpGestureStarted() {
             verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
                     notifySwipeUpGestureStartedInternal());
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
index 55602a9..e3658de 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
@@ -19,6 +19,7 @@
 import static android.os.FileUtils.closeQuietly;
 
 import android.annotation.IntRange;
+import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.graphics.Bitmap;
@@ -29,6 +30,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.provider.MediaStore;
 import android.util.Log;
 
@@ -142,8 +144,9 @@
      *
      * @return a listenable future result
      */
-    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) {
-        return export(executor, requestId, bitmap, ZonedDateTime.now());
+    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
+            UserHandle owner) {
+        return export(executor, requestId, bitmap, ZonedDateTime.now(), owner);
     }
 
     /**
@@ -155,10 +158,10 @@
      * @return a listenable future result
      */
     ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
-            ZonedDateTime captureTime) {
+            ZonedDateTime captureTime, UserHandle owner) {
 
         final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
-                mQuality, /* publish */ true);
+                mQuality, /* publish */ true, owner);
 
         return CallbackToFutureAdapter.getFuture(
                 (completer) -> {
@@ -174,28 +177,6 @@
         );
     }
 
-    /**
-     * Delete the entry.
-     *
-     * @param executor the thread for execution
-     * @param uri the uri of the image to publish
-     *
-     * @return a listenable future result
-     */
-    ListenableFuture<Result> delete(Executor executor, Uri uri) {
-        return CallbackToFutureAdapter.getFuture((completer) -> {
-            executor.execute(() -> {
-                mResolver.delete(uri, null);
-
-                Result result = new Result();
-                result.uri = uri;
-                result.deleted = true;
-                completer.set(result);
-            });
-            return "ContentResolver#delete";
-        });
-    }
-
     static class Result {
         Uri uri;
         UUID requestId;
@@ -203,7 +184,6 @@
         long timestamp;
         CompressFormat format;
         boolean published;
-        boolean deleted;
 
         @Override
         public String toString() {
@@ -214,7 +194,6 @@
             sb.append(", timestamp=").append(timestamp);
             sb.append(", format=").append(format);
             sb.append(", published=").append(published);
-            sb.append(", deleted=").append(deleted);
             sb.append('}');
             return sb.toString();
         }
@@ -227,17 +206,19 @@
         private final ZonedDateTime mCaptureTime;
         private final CompressFormat mFormat;
         private final int mQuality;
+        private final UserHandle mOwner;
         private final String mFileName;
         private final boolean mPublish;
 
         Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
-                CompressFormat format, int quality, boolean publish) {
+                CompressFormat format, int quality, boolean publish, UserHandle owner) {
             mResolver = resolver;
             mRequestId = requestId;
             mBitmap = bitmap;
             mCaptureTime = captureTime;
             mFormat = format;
             mQuality = quality;
+            mOwner = owner;
             mFileName = createFilename(mCaptureTime, mFormat);
             mPublish = publish;
         }
@@ -253,7 +234,7 @@
                     start = Instant.now();
                 }
 
-                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName);
+                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner);
                 throwIfInterrupted();
 
                 writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -297,15 +278,20 @@
     }
 
     private static Uri createEntry(ContentResolver resolver, CompressFormat format,
-            ZonedDateTime time, String fileName) throws ImageExportException {
+            ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException {
         Trace.beginSection("ImageExporter_createEntry");
         try {
             final ContentValues values = createMetadata(time, format, fileName);
 
-            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+            Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+            if (UserHandle.myUserId() != owner.getIdentifier()) {
+                baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());
+            }
+            Uri uri = resolver.insert(baseUri, values);
             if (uri == null) {
                 throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
             }
+            Log.d(TAG, "Inserted new URI: " + uri);
             return uri;
         } finally {
             Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index ba6e98e..8bf956b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -30,6 +30,7 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
@@ -387,7 +388,9 @@
 
         mOutputBitmap = renderBitmap(drawable, bounds);
         ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
-                mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now());
+                mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
+                // TODO: Owner must match the owner of the captured window.
+                Process.myUserHandle());
         exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index f248d69..077ad35 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -48,6 +48,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -71,6 +73,7 @@
     private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
 
     private final Context mContext;
+    private FeatureFlags mFlags;
     private final ScreenshotSmartActions mScreenshotSmartActions;
     private final ScreenshotController.SaveImageInBackgroundData mParams;
     private final ScreenshotController.SavedImageData mImageData;
@@ -84,7 +87,10 @@
     private final ImageExporter mImageExporter;
     private long mImageTime;
 
-    SaveImageInBackgroundTask(Context context, ImageExporter exporter,
+    SaveImageInBackgroundTask(
+            Context context,
+            FeatureFlags flags,
+            ImageExporter exporter,
             ScreenshotSmartActions screenshotSmartActions,
             ScreenshotController.SaveImageInBackgroundData data,
             Supplier<ActionTransition> sharedElementTransition,
@@ -92,6 +98,7 @@
                     screenshotNotificationSmartActionsProvider
     ) {
         mContext = context;
+        mFlags = flags;
         mScreenshotSmartActions = screenshotSmartActions;
         mImageData = new ScreenshotController.SavedImageData();
         mQuickShareData = new ScreenshotController.QuickShareData();
@@ -117,7 +124,8 @@
         }
         // TODO: move to constructor / from ScreenshotRequest
         final UUID requestId = UUID.randomUUID();
-        final UserHandle user = getUserHandleOfForegroundApplication(mContext);
+        final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+                ? mParams.owner : getUserHandleOfForegroundApplication(mContext);
 
         Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
 
@@ -133,8 +141,9 @@
 
             // Call synchronously here since already on a background thread.
             ListenableFuture<ImageExporter.Result> future =
-                    mImageExporter.export(Runnable::run, requestId, image);
+                    mImageExporter.export(Runnable::run, requestId, image, mParams.owner);
             ImageExporter.Result result = future.get();
+            Log.d(TAG, "Saved screenshot: " + result);
             final Uri uri = result.uri;
             mImageTime = result.timestamp;
 
@@ -157,6 +166,7 @@
             }
 
             mImageData.uri = uri;
+            mImageData.owner = user;
             mImageData.smartActions = smartActions;
             mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
             mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 3fee232..df32d20 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -34,6 +34,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.annotation.MainThread;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -57,7 +58,9 @@
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -90,6 +93,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
 import com.android.systemui.util.Assert;
@@ -151,6 +155,7 @@
         public Consumer<Uri> finisher;
         public ScreenshotController.ActionsReadyListener mActionsReadyListener;
         public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
+        public UserHandle owner;
 
         void clearImage() {
             image = null;
@@ -167,6 +172,8 @@
         public Notification.Action deleteAction;
         public List<Notification.Action> smartActions;
         public Notification.Action quickShareAction;
+        public UserHandle owner;
+
 
         /**
          * POD for shared element transition.
@@ -242,6 +249,7 @@
     private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
 
     private final WindowContext mContext;
+    private final FeatureFlags mFlags;
     private final ScreenshotNotificationsController mNotificationsController;
     private final ScreenshotSmartActions mScreenshotSmartActions;
     private final UiEventLogger mUiEventLogger;
@@ -288,6 +296,7 @@
     @Inject
     ScreenshotController(
             Context context,
+            FeatureFlags flags,
             ScreenshotSmartActions screenshotSmartActions,
             ScreenshotNotificationsController screenshotNotificationsController,
             ScrollCaptureClient scrollCaptureClient,
@@ -331,6 +340,7 @@
         final Context displayContext = context.createDisplayContext(getDefaultDisplay());
         mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
         mWindowManager = mContext.getSystemService(WindowManager.class);
+        mFlags = flags;
 
         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
 
@@ -377,7 +387,6 @@
     void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
             Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
             Consumer<Uri> finisher, RequestCallback requestCallback) {
-        // TODO: use task Id, userId, topComponent for smart handler
         Assert.isMainThread();
         if (screenshot == null) {
             Log.e(TAG, "Got null bitmap from screenshot message");
@@ -395,7 +404,7 @@
         }
         mCurrentRequestCallback = requestCallback;
         saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
-                showFlash);
+                showFlash, UserHandle.of(userId));
     }
 
     /**
@@ -543,14 +552,15 @@
             return;
         }
 
-        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
+        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true,
+                Process.myUserHandle());
 
         mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
                 ClipboardOverlayController.SELF_PERMISSION);
     }
 
     private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
-            Insets screenInsets, ComponentName topComponent, boolean showFlash) {
+            Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) {
         withWindowAttached(() ->
                 mScreenshotView.announceForAccessibility(
                         mContext.getResources().getString(R.string.screenshot_saving_title)));
@@ -575,11 +585,11 @@
 
         mScreenBitmap = screenshot;
 
-        if (!isUserSetupComplete()) {
+        if (!isUserSetupComplete(owner)) {
             Log.w(TAG, "User setup not complete, displaying toast only");
             // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
             // and sharing shouldn't be exposed to the user.
-            saveScreenshotAndToast(finisher);
+            saveScreenshotAndToast(owner, finisher);
             return;
         }
 
@@ -587,7 +597,7 @@
         mScreenBitmap.setHasAlpha(false);
         mScreenBitmap.prepareToDraw();
 
-        saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
+        saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady,
                 this::showUiOnQuickShareActionReady);
 
         // The window is focusable by default
@@ -853,11 +863,12 @@
      * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
      * failure).
      */
-    private void saveScreenshotAndToast(Consumer<Uri> finisher) {
+    private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
         // Play the shutter sound to notify that we've taken a screenshot
         playCameraSound();
 
         saveScreenshotInWorkerThread(
+                owner,
                 /* onComplete */ finisher,
                 /* actionsReadyListener */ imageData -> {
                     if (DEBUG_CALLBACK) {
@@ -925,9 +936,11 @@
     /**
      * Creates a new worker thread and saves the screenshot to the media store.
      */
-    private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
-            @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
-            @Nullable ScreenshotController.QuickShareActionReadyListener
+    private void saveScreenshotInWorkerThread(
+            UserHandle owner,
+            @NonNull Consumer<Uri> finisher,
+            @Nullable ActionsReadyListener actionsReadyListener,
+            @Nullable QuickShareActionReadyListener
                     quickShareActionsReadyListener) {
         ScreenshotController.SaveImageInBackgroundData
                 data = new ScreenshotController.SaveImageInBackgroundData();
@@ -935,13 +948,14 @@
         data.finisher = finisher;
         data.mActionsReadyListener = actionsReadyListener;
         data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
+        data.owner = owner;
 
         if (mSaveInBgTask != null) {
             // just log success/failure for the pre-existing screenshot
             mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
         }
 
-        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
+        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
                 mScreenshotSmartActions, data, getActionTransitionSupplier(),
                 mScreenshotNotificationSmartActionsProvider);
         mSaveInBgTask.execute();
@@ -960,6 +974,15 @@
         mScreenshotHandler.resetTimeout();
 
         if (imageData.uri != null) {
+            if (!imageData.owner.equals(Process.myUserHandle())) {
+                // TODO: Handle non-primary user ownership (e.g. Work Profile)
+                // This image is owned by another user. Special treatment will be
+                // required in the UI (badging) as well as sending intents which can
+                // correctly forward those URIs on to be read (actions).
+
+                Log.d(TAG, "*** Screenshot saved to a non-primary user ("
+                        + imageData.owner + ") as " + imageData.uri);
+            }
             mScreenshotHandler.post(() -> {
                 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
                     mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@@ -1033,9 +1056,9 @@
         }
     }
 
-    private boolean isUserSetupComplete() {
-        return Settings.Secure.getInt(mContext.getContentResolver(),
-                SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+    private boolean isUserSetupComplete(UserHandle owner) {
+        return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
+                        .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index c2a5060..3a35286 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -68,7 +68,9 @@
     }
 
     override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
-        return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+        val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+        Log.d(TAG, "isManagedProfile: $managed")
+        return managed
     }
 
     private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 83b60fb..30a0b8f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -78,6 +78,7 @@
     static class LongScreenshot {
         private final ImageTileSet mImageTileSet;
         private final Session mSession;
+        // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy
 
         LongScreenshot(Session session, ImageTileSet imageTileSet) {
             mSession = session;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 7254e09..1110386 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -425,8 +425,6 @@
             new KeyguardClockPositionAlgorithm.Result();
     private boolean mIsExpanding;
 
-    private boolean mBlockTouches;
-
     /**
      * Determines if QS should be already expanded when expanding shade.
      * Used for split shade, two finger gesture as well as accessibility shortcut to QS.
@@ -1693,7 +1691,6 @@
 
     public void resetViews(boolean animate) {
         mIsLaunchTransitionFinished = false;
-        mBlockTouches = false;
         mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */,
                 true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
         if (animate && !isFullyCollapsed()) {
@@ -4198,7 +4195,7 @@
                             "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
                                     + "," + event.getY() + ")");
                 }
-                if (mBlockTouches || mQs.disallowPanelTouches()) {
+                if (mQs.disallowPanelTouches()) {
                     return false;
                 }
                 initDownStates(event);
@@ -4241,8 +4238,7 @@
                 }
 
 
-                if (mBlockTouches || (mQsFullyExpanded && mQs != null
-                        && mQs.disallowPanelTouches())) {
+                if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
                     return false;
                 }
 
@@ -4700,6 +4696,8 @@
                 if (!animatingUnlockedShadeToKeyguard) {
                     // Only make the status bar visible if we're not animating the screen off, since
                     // we only want to be showing the clock/notifications during the animation.
+                    mShadeLog.v("Updating keyguard status bar state to "
+                            + (keyguardShowing ? "visible" : "invisible"));
                     mKeyguardStatusBarViewController.updateViewState(
                             /* alpha= */ 1f,
                             keyguardShowing ? View.VISIBLE : View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 8699441..c290ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -42,7 +42,6 @@
 
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -97,6 +96,7 @@
     private final List<UserChangedListener> mListeners = new ArrayList<>();
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final NotificationClickNotifier mClickNotifier;
+    private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy;
 
     private boolean mShowLockscreenNotifications;
     private boolean mAllowLockscreenRemoteInput;
@@ -157,7 +157,7 @@
                     break;
                 case Intent.ACTION_USER_UNLOCKED:
                     // Start the overview connection to the launcher service
-                    Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+                    mOverviewProxyServiceLazy.get().startConnectionToCurrentUser();
                     break;
                 case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION:
                     final IntentSender intentSender = intent.getParcelableExtra(
@@ -199,6 +199,7 @@
             Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
             Lazy<CommonNotifCollection> commonNotifCollectionLazy,
             NotificationClickNotifier clickNotifier,
+            Lazy<OverviewProxyService> overviewProxyServiceLazy,
             KeyguardManager keyguardManager,
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
@@ -214,6 +215,7 @@
         mVisibilityProviderLazy = visibilityProviderLazy;
         mCommonNotifCollectionLazy = commonNotifCollectionLazy;
         mClickNotifier = clickNotifier;
+        mOverviewProxyServiceLazy = overviewProxyServiceLazy;
         statusBarStateController.addCallback(this);
         mLockPatternUtils = new LockPatternUtils(context);
         mKeyguardManager = keyguardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index c900c5a..4be5a1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -43,7 +43,6 @@
 import android.view.View;
 import android.widget.ImageView;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -89,11 +88,9 @@
     private static final String TAG = "NotificationMediaManager";
     public static final boolean DEBUG_MEDIA = false;
 
-    private final StatusBarStateController mStatusBarStateController
-            = Dependency.get(StatusBarStateController.class);
-    private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
-    private final KeyguardStateController mKeyguardStateController = Dependency.get(
-            KeyguardStateController.class);
+    private final StatusBarStateController mStatusBarStateController;
+    private final SysuiColorExtractor mColorExtractor;
+    private final KeyguardStateController mKeyguardStateController;
     private final KeyguardBypassController mKeyguardBypassController;
     private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
     private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
@@ -179,6 +176,9 @@
             NotifCollection notifCollection,
             @Main DelayableExecutor mainExecutor,
             MediaDataManager mediaDataManager,
+            StatusBarStateController statusBarStateController,
+            SysuiColorExtractor colorExtractor,
+            KeyguardStateController keyguardStateController,
             DumpManager dumpManager) {
         mContext = context;
         mMediaArtworkProcessor = mediaArtworkProcessor;
@@ -192,6 +192,9 @@
         mMediaDataManager = mediaDataManager;
         mNotifPipeline = notifPipeline;
         mNotifCollection = notifCollection;
+        mStatusBarStateController = statusBarStateController;
+        mColorExtractor = colorExtractor;
+        mKeyguardStateController = keyguardStateController;
 
         setupNotifPipeline();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 7cd79ca..11e3d17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -26,6 +26,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -130,6 +131,9 @@
             NotifCollection notifCollection,
             @Main DelayableExecutor mainExecutor,
             MediaDataManager mediaDataManager,
+            StatusBarStateController statusBarStateController,
+            SysuiColorExtractor colorExtractor,
+            KeyguardStateController keyguardStateController,
             DumpManager dumpManager) {
         return new NotificationMediaManager(
                 context,
@@ -142,6 +146,9 @@
                 notifCollection,
                 mainExecutor,
                 mediaDataManager,
+                statusBarStateController,
+                colorExtractor,
+                keyguardStateController,
                 dumpManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8278b54..ccf6fec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -393,7 +393,7 @@
             val posted = mPostedEntries.compute(entry.key) { _, value ->
                 value?.also { update ->
                     update.wasUpdated = true
-                    update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver
+                    update.shouldHeadsUpEver = shouldHeadsUpEver
                     update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
                     update.isAlerting = isAlerting
                     update.isBinding = isBinding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 801e544..8eef3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -19,6 +19,8 @@
 import android.service.notification.StatusBarNotification
 import com.android.systemui.ForegroundServiceNotificationListener
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
 import com.android.systemui.statusbar.NotificationListener
@@ -38,6 +40,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
 import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -71,6 +74,8 @@
     private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
     private val bubblesOptional: Optional<Bubbles>,
     private val fgsNotifListener: ForegroundServiceNotificationListener,
+    private val memoryMonitor: Lazy<NotificationMemoryMonitor>,
+    private val featureFlags: FeatureFlags
 ) : NotificationsController {
 
     override fun initialize(
@@ -112,6 +117,9 @@
         notificationLogger.setUpWithContainer(listContainer)
         peopleSpaceWidgetManager.attach(notificationListener)
         fgsNotifListener.init()
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) {
+            memoryMonitor.get().init()
+        }
     }
 
     // TODO: Convert all functions below this line into listeners instead of public methods
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
new file mode 100644
index 0000000..832a739
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -0,0 +1,41 @@
+/*
+ *
+ * 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.statusbar.notification.logging
+
+/** Describes usage of a notification. */
+data class NotificationMemoryUsage(
+    val packageName: String,
+    val notificationId: String,
+    val objectUsage: NotificationObjectUsage,
+)
+
+/**
+ * Describes current memory usage of a [android.app.Notification] object.
+ *
+ * The values are in bytes.
+ */
+data class NotificationObjectUsage(
+    val smallIcon: Int,
+    val largeIcon: Int,
+    val extras: Int,
+    val style: String?,
+    val styleIcon: Int,
+    val bigPicture: Int,
+    val extender: Int,
+    val hasCustomView: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
new file mode 100644
index 0000000..ef7fa33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -0,0 +1,239 @@
+/*
+ *
+ * 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.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
+import androidx.annotation.WorkerThread
+import androidx.core.util.contains
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** This class monitors and logs current Notification memory use. */
+@SysUISingleton
+class NotificationMemoryMonitor
+@Inject
+constructor(
+    val notificationPipeline: NotifPipeline,
+    val dumpManager: DumpManager,
+) : Dumpable {
+
+    companion object {
+        private const val TAG = "NotificationMemMonitor"
+        private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+        private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+        private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+        private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+        private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+    }
+
+    fun init() {
+        Log.d(TAG, "NotificationMemoryMonitor initialized.")
+        dumpManager.registerDumpable(javaClass.simpleName, this)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+    }
+
+    @WorkerThread
+    fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
+        return notificationMemoryUse(notificationPipeline.allNotifs)
+    }
+
+    /** Returns a list of memory use entries for currently shown notifications. */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notifications: Collection<NotificationEntry>
+    ): List<NotificationMemoryUsage> {
+        return notifications.asSequence().map { entry ->
+            val packageName = entry.sbn.packageName
+            val notificationObjectUsage =
+                computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
+            NotificationMemoryUsage(
+                packageName,
+                NotificationUtils.logKey(entry.sbn.key),
+                notificationObjectUsage)
+        }.toList()
+    }
+
+    /**
+     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+     * inspect Bitmaps in the object and provide summary of memory usage.
+     */
+    private fun computeNotificationObjectUse(
+        notification: Notification,
+        seenBitmaps: HashSet<Int>
+    ): NotificationObjectUsage {
+        val extras = notification.extras
+        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+        // Collect memory usage of extra styles
+
+        // Big Picture
+        val bigPictureIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+        val bigPictureUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+        // People
+        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+        val peopleUse =
+            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+        // Calling
+        val callingPersonUse =
+            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+        val verificationIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+        // Messages
+        val messages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+            )
+        val messagesUse =
+            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+        val historicMessages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+            )
+        val historyicMessagesUse =
+            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+        // Extenders
+        val carExtender = extras.getBundle(CAR_EXTENSIONS)
+        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+        val carExtenderIcon =
+            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+        val tvExtender = extras.getBundle(TV_EXTENSIONS)
+        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+        val wearExtenderBackground =
+            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+        val style = notification.notificationStyle
+        val hasCustomView = notification.contentView != null || notification.bigContentView != null
+        val extrasSize = computeBundleSize(extras)
+
+        return NotificationObjectUsage(
+            smallIconUse,
+            largeIconUse,
+            extrasSize,
+            style?.simpleName,
+            bigPictureIconUse +
+                peopleUse +
+                callingPersonUse +
+                verificationIconUse +
+                messagesUse +
+                historyicMessagesUse,
+            bigPictureUse,
+            carExtenderSize +
+                carExtenderIcon +
+                tvExtenderSize +
+                wearExtenderSize +
+                wearExtenderBackground,
+            hasCustomView
+        )
+    }
+
+    /**
+     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+     * bitmaps). Can be slow.
+     */
+    private fun computeBundleSize(extras: Bundle): Int {
+        val parcel = Parcel.obtain()
+        try {
+            extras.writeToParcel(parcel, 0)
+            return parcel.dataSize()
+        } finally {
+            parcel.recycle()
+        }
+    }
+
+    /**
+     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+     * if the key does not exist in extras.
+     */
+    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+            is Icon -> computeIconUse(parcelable, seenBitmaps)
+            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+            else -> 0
+        }
+    }
+
+    /**
+     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+     * defined via Uri or a resource.
+     *
+     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+     */
+    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+        when (icon?.type) {
+            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+            else -> 0
+        }
+
+    /**
+     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+     * seenBitmaps set, this method returns 0 to avoid double counting.
+     *
+     * @return memory usage of the bitmap in bytes
+     */
+    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+        val refId = System.identityHashCode(bitmap)
+        if (seenBitmaps?.contains(refId) == true) {
+            return 0
+        }
+
+        seenBitmaps?.add(refId)
+        return bitmap.allocationByteCount
+    }
+
+    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+        val refId = System.identityHashCode(icon.dataBytes)
+        if (seenBitmaps.contains(refId)) {
+            return 0
+        }
+
+        seenBitmaps.add(refId)
+        return icon.dataLength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 9faef1b..5ca13c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -45,11 +45,21 @@
     void logPanelShown(boolean isLockscreen,
             @Nullable List<NotificationEntry> visibleNotifications);
 
+    /**
+     * Log a NOTIFICATION_PANEL_REPORTED statsd event, with
+     * {@link NotificationPanelEvent#NOTIFICATION_DRAG} as the eventID.
+     *
+     * @param draggedNotification the notification that is being dragged
+     */
+    void logNotificationDrag(NotificationEntry draggedNotification);
+
     enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
         @UiEvent(doc = "Notification panel shown from status bar.")
         NOTIFICATION_PANEL_OPEN_STATUS_BAR(200),
         @UiEvent(doc = "Notification panel shown from lockscreen.")
-        NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201);
+        NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201),
+        @UiEvent(doc = "Notification was dragged")
+        NOTIFICATION_DRAG(1226);
 
         private final int mId;
         NotificationPanelEvent(int id) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
index 75a6019..9a63228 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
@@ -16,12 +16,15 @@
 
 package com.android.systemui.statusbar.notification.logging;
 
+import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG;
+
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.nano.Notifications;
 
 import com.google.protobuf.nano.MessageNano;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -38,4 +41,14 @@
                 /* int num_notifications*/ proto.notifications.length,
                 /* byte[] notifications*/ MessageNano.toByteArray(proto));
     }
+
+    @Override
+    public void logNotificationDrag(NotificationEntry draggedNotification) {
+        final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
+                Collections.singletonList(draggedNotification));
+        SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
+                /* int event_id */ NOTIFICATION_DRAG.getId(),
+                /* int num_notifications*/ proto.notifications.length,
+                /* byte[] notifications*/ MessageNano.toByteArray(proto));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
index 4939a9c..64f87ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -45,12 +45,17 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
+import java.util.Collections;
+
 import javax.inject.Inject;
 
 /**
@@ -63,14 +68,17 @@
     private final Context mContext;
     private final HeadsUpManager mHeadsUpManager;
     private final ShadeController mShadeController;
+    private NotificationPanelLogger mNotificationPanelLogger;
 
     @Inject
     public ExpandableNotificationRowDragController(Context context,
             HeadsUpManager headsUpManager,
-            ShadeController shadeController) {
+            ShadeController shadeController,
+            NotificationPanelLogger notificationPanelLogger) {
         mContext = context;
         mHeadsUpManager = headsUpManager;
         mShadeController = shadeController;
+        mNotificationPanelLogger = notificationPanelLogger;
 
         init();
     }
@@ -120,12 +128,16 @@
         dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent);
         dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
         ClipData.Item item = new ClipData.Item(dragIntent);
+        InstanceId instanceId = new InstanceIdSequence(Integer.MAX_VALUE).newInstanceId();
+        item.getIntent().putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, instanceId);
         ClipData dragData = new ClipData(clipDescription, item);
         View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
         view.setOnDragListener(getDraggedViewDragListener());
         boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL
                 | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
         if (result) {
+            // Log notification drag only if it succeeds
+            mNotificationPanelLogger.logNotificationDrag(enr.getEntry());
             view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
             if (enr.isPinned()) {
                 mHeadsUpManager.releaseAllImmediately();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index f7ce43b..b2c7a82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -4434,10 +4434,11 @@
                     Trace.beginSection("CentralSurfaces#updateDozing");
                     mDozing = isDozing;
 
-                    // Collapse the notification panel if open
                     boolean dozingAnimated = mDozeServiceHost.getDozingRequested()
                             && mDozeParameters.shouldControlScreenOff();
-                    mNotificationPanelViewController.resetViews(dozingAnimated);
+                    // resetting views is already done when going into doze, there's no need to
+                    // reset them again when we're waking up
+                    mNotificationPanelViewController.resetViews(dozingAnimated && isDozing);
 
                     updateQsExpansionEnabled();
                     mKeyguardViewMediator.setDozing(mDozing);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
index 7de4668..0067316 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.os.Handler;
-import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.SysUISingleton;
@@ -34,9 +33,6 @@
  */
 @SysUISingleton
 public class DozeScrimController implements StateListener {
-    private static final String TAG = "DozeScrimController";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
     private final DozeLog mDozeLog;
     private final DozeParameters mDozeParameters;
     private final Handler mHandler = new Handler();
@@ -44,28 +40,26 @@
     private boolean mDozing;
     private DozeHost.PulseCallback mPulseCallback;
     private int mPulseReason;
-    private boolean mFullyPulsing;
 
     private final ScrimController.Callback mScrimCallback = new ScrimController.Callback() {
         @Override
         public void onDisplayBlanked() {
-            if (DEBUG) {
-                Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason="
-                        + DozeLog.reasonToString(mPulseReason));
-            }
             if (!mDozing) {
+                mDozeLog.tracePulseDropped("onDisplayBlanked - not dozing");
                 return;
             }
 
-            // Signal that the pulse is ready to turn the screen on and draw.
-            pulseStarted();
+            if (mPulseCallback != null) {
+                // Signal that the pulse is ready to turn the screen on and draw.
+                mDozeLog.tracePulseStart(mPulseReason);
+                mPulseCallback.onPulseStarted();
+            }
         }
 
         @Override
         public void onFinished() {
-            if (DEBUG) {
-                Log.d(TAG, "Pulse in finished, mDozing=" + mDozing);
-            }
+            mDozeLog.tracePulseEvent("scrimCallback-onFinished", mDozing, mPulseReason);
+
             if (!mDozing) {
                 return;
             }
@@ -78,7 +72,6 @@
                 mHandler.postDelayed(mPulseOutExtended,
                         mDozeParameters.getPulseVisibleDurationExtended());
             }
-            mFullyPulsing = true;
         }
 
         /**
@@ -118,19 +111,14 @@
         }
 
         if (!mDozing || mPulseCallback != null) {
-            if (DEBUG) {
-                Log.d(TAG, "Pulse suppressed. Dozing: " + mDozeParameters + " had callback? "
-                        + (mPulseCallback != null));
-            }
             // Pulse suppressed.
             callback.onPulseFinished();
             if (!mDozing) {
-                mDozeLog.tracePulseDropped("device isn't dozing");
+                mDozeLog.tracePulseDropped("pulse - device isn't dozing");
             } else {
-                mDozeLog.tracePulseDropped("already has pulse callback mPulseCallback="
+                mDozeLog.tracePulseDropped("pulse - already has pulse callback mPulseCallback="
                         + mPulseCallback);
             }
-
             return;
         }
 
@@ -141,9 +129,7 @@
     }
 
     public void pulseOutNow() {
-        if (mPulseCallback != null && mFullyPulsing) {
-            mPulseOut.run();
-        }
+        mPulseOut.run();
     }
 
     public boolean isPulsing() {
@@ -168,24 +154,16 @@
 
     private void cancelPulsing() {
         if (mPulseCallback != null) {
-            if (DEBUG) Log.d(TAG, "Cancel pulsing");
-            mFullyPulsing = false;
+            mDozeLog.tracePulseEvent("cancel", mDozing, mPulseReason);
             mHandler.removeCallbacks(mPulseOut);
             mHandler.removeCallbacks(mPulseOutExtended);
             pulseFinished();
         }
     }
 
-    private void pulseStarted() {
-        mDozeLog.tracePulseStart(mPulseReason);
-        if (mPulseCallback != null) {
-            mPulseCallback.onPulseStarted();
-        }
-    }
-
     private void pulseFinished() {
-        mDozeLog.tracePulseFinish();
         if (mPulseCallback != null) {
+            mDozeLog.tracePulseFinish();
             mPulseCallback.onPulseFinished();
             mPulseCallback = null;
         }
@@ -202,10 +180,9 @@
     private final Runnable mPulseOut = new Runnable() {
         @Override
         public void run() {
-            mFullyPulsing = false;
             mHandler.removeCallbacks(mPulseOut);
             mHandler.removeCallbacks(mPulseOutExtended);
-            if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing);
+            mDozeLog.tracePulseEvent("out", mDozing, mPulseReason);
             if (!mDozing) return;
             pulseFinished();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 24ce5e9..5196e10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -36,7 +36,6 @@
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.doze.DozeReceiver;
-import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -48,6 +47,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.util.Assert;
 
 import java.util.ArrayList;
@@ -80,7 +80,6 @@
     private final BatteryController mBatteryController;
     private final ScrimController mScrimController;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
-    private final KeyguardViewMediator mKeyguardViewMediator;
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final DozeScrimController mDozeScrimController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -95,6 +94,7 @@
     private View mAmbientIndicationContainer;
     private CentralSurfaces mCentralSurfaces;
     private boolean mAlwaysOnSuppressed;
+    private boolean mPulsePending;
 
     @Inject
     public DozeServiceHost(DozeLog dozeLog, PowerManager powerManager,
@@ -104,7 +104,6 @@
             HeadsUpManagerPhone headsUpManagerPhone, BatteryController batteryController,
             ScrimController scrimController,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
-            KeyguardViewMediator keyguardViewMediator,
             Lazy<AssistManager> assistManagerLazy,
             DozeScrimController dozeScrimController, KeyguardUpdateMonitor keyguardUpdateMonitor,
             PulseExpansionHandler pulseExpansionHandler,
@@ -122,7 +121,6 @@
         mBatteryController = batteryController;
         mScrimController = scrimController;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
-        mKeyguardViewMediator = keyguardViewMediator;
         mAssistManagerLazy = assistManagerLazy;
         mDozeScrimController = dozeScrimController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -131,6 +129,7 @@
         mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
         mAuthController = authController;
         mNotificationIconAreaController = notificationIconAreaController;
+        mHeadsUpManagerPhone.addListener(mOnHeadsUpChangedListener);
     }
 
     // TODO: we should try to not pass status bar in here if we can avoid it.
@@ -246,7 +245,7 @@
         mDozeScrimController.pulse(new PulseCallback() {
             @Override
             public void onPulseStarted() {
-                callback.onPulseStarted();
+                callback.onPulseStarted(); // requestState(DozeMachine.State.DOZE_PULSING)
                 mCentralSurfaces.updateNotificationPanelTouchState();
                 setPulsing(true);
             }
@@ -254,7 +253,7 @@
             @Override
             public void onPulseFinished() {
                 mPulsing = false;
-                callback.onPulseFinished();
+                callback.onPulseFinished(); // requestState(DozeMachine.State.DOZE_PULSE_DONE)
                 mCentralSurfaces.updateNotificationPanelTouchState();
                 mScrimController.setWakeLockScreenSensorActive(false);
                 setPulsing(false);
@@ -338,9 +337,8 @@
 
     @Override
     public void stopPulsing() {
-        if (mDozeScrimController.isPulsing()) {
-            mDozeScrimController.pulseOutNow();
-        }
+        setPulsePending(false); // prevent any pending pulses from continuing
+        mDozeScrimController.pulseOutNow();
     }
 
     @Override
@@ -451,6 +449,16 @@
         }
     }
 
+    @Override
+    public boolean isPulsePending() {
+        return mPulsePending;
+    }
+
+    @Override
+    public void setPulsePending(boolean isPulsePending) {
+        mPulsePending = isPulsePending;
+    }
+
     /**
      * Whether always-on-display is being suppressed. This does not affect wakeup gestures like
      * pickup and tap.
@@ -458,4 +466,22 @@
     public boolean isAlwaysOnSuppressed() {
         return mAlwaysOnSuppressed;
     }
+
+    final OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() {
+        @Override
+        public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+            if (mStatusBarStateController.isDozing() && isHeadsUp) {
+                entry.setPulseSuppressed(false);
+                fireNotificationPulse(entry);
+                if (isPulsing()) {
+                    mDozeScrimController.cancelPendingPulseTimeout();
+                }
+            }
+            if (!isHeadsUp && !mHeadsUpManagerPhone.hasNotifications()) {
+                // There are no longer any notifications to show.  We should end the
+                // pulse now.
+                stopPulsing();
+            }
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 0026b71..054bd28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -40,6 +40,7 @@
 import com.android.keyguard.CarrierTextController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.battery.BatteryMeterViewController;
@@ -116,6 +117,7 @@
     private final CommandQueue mCommandQueue;
     private final Executor mMainExecutor;
     private final Object mLock = new Object();
+    private final KeyguardLogger mLogger;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -279,7 +281,8 @@
             StatusBarUserInfoTracker statusBarUserInfoTracker,
             SecureSettings secureSettings,
             CommandQueue commandQueue,
-            @Main Executor mainExecutor
+            @Main Executor mainExecutor,
+            KeyguardLogger logger
     ) {
         super(view);
         mCarrierTextController = carrierTextController;
@@ -304,6 +307,7 @@
         mSecureSettings = secureSettings;
         mCommandQueue = commandQueue;
         mMainExecutor = mainExecutor;
+        mLogger = logger;
 
         mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
         mKeyguardStateController.addCallback(
@@ -430,6 +434,7 @@
 
     /** Animate the keyguard status bar in. */
     public void animateKeyguardStatusBarIn() {
+        mLogger.d("animating status bar in");
         if (mDisableStateTracker.isDisabled()) {
             // If our view is disabled, don't allow us to animate in.
             return;
@@ -445,6 +450,7 @@
 
     /** Animate the keyguard status bar out. */
     public void animateKeyguardStatusBarOut(long startDelay, long duration) {
+        mLogger.d("animating status bar out");
         ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
         anim.addUpdateListener(mAnimatorUpdateListener);
         anim.setStartDelay(startDelay);
@@ -481,6 +487,9 @@
             newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
                     * mKeyguardStatusBarAnimateAlpha
                     * (1.0f - mKeyguardHeadsUpShowingAmount);
+            if (newAlpha != mView.getAlpha() && (newAlpha == 0 || newAlpha == 1)) {
+                mLogger.logStatusBarCalculatedAlpha(newAlpha);
+            }
         }
 
         boolean hideForBypass =
@@ -503,6 +512,10 @@
         if (mDisableStateTracker.isDisabled()) {
             visibility = View.INVISIBLE;
         }
+        if (visibility != mView.getVisibility()) {
+            mLogger.logStatusBarAlphaVisibility(visibility, alpha,
+                    StatusBarState.toString(mStatusBarState));
+        }
         mView.setAlpha(alpha);
         mView.setVisibility(visibility);
     }
@@ -596,6 +609,8 @@
         pw.println("KeyguardStatusBarView:");
         pw.println("  mBatteryListening: " + mBatteryListening);
         pw.println("  mExplicitAlpha: " + mExplicitAlpha);
+        pw.println("  alpha: " + mView.getAlpha());
+        pw.println("  visibility: " + mView.getVisibility());
         mView.dump(pw, args);
     }
 
@@ -605,6 +620,10 @@
      * @param alpha a value between 0 and 1. -1 if the value is to be reset/ignored.
      */
     public void setAlpha(float alpha) {
+        if (mExplicitAlpha != alpha && (mExplicitAlpha == -1 || alpha == -1)) {
+            // logged if value changed to ignored or from ignored
+            mLogger.logStatusBarExplicitAlpha(alpha);
+        }
         mExplicitAlpha = alpha;
         updateViewState();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
index ae201e3..5512bed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
@@ -21,8 +21,6 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
@@ -41,9 +39,6 @@
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final StatusBarStateController mStatusBarStateController;
     private final NotificationRemoteInputManager mNotificationRemoteInputManager;
-    private final NotificationsController mNotificationsController;
-    private final DozeServiceHost mDozeServiceHost;
-    private final DozeScrimController mDozeScrimController;
 
     @Inject
     StatusBarHeadsUpChangeListener(
@@ -53,10 +48,7 @@
             KeyguardBypassController keyguardBypassController,
             HeadsUpManagerPhone headsUpManager,
             StatusBarStateController statusBarStateController,
-            NotificationRemoteInputManager notificationRemoteInputManager,
-            NotificationsController notificationsController,
-            DozeServiceHost dozeServiceHost,
-            DozeScrimController dozeScrimController) {
+            NotificationRemoteInputManager notificationRemoteInputManager) {
 
         mNotificationShadeWindowController = notificationShadeWindowController;
         mStatusBarWindowController = statusBarWindowController;
@@ -65,9 +57,6 @@
         mHeadsUpManager = headsUpManager;
         mStatusBarStateController = statusBarStateController;
         mNotificationRemoteInputManager = notificationRemoteInputManager;
-        mNotificationsController = notificationsController;
-        mDozeServiceHost = dozeServiceHost;
-        mDozeScrimController = dozeScrimController;
     }
 
     @Override
@@ -117,20 +106,4 @@
             }
         }
     }
-
-    @Override
-    public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
-        if (mStatusBarStateController.isDozing() && isHeadsUp) {
-            entry.setPulseSuppressed(false);
-            mDozeServiceHost.fireNotificationPulse(entry);
-            if (mDozeServiceHost.isPulsing()) {
-                mDozeScrimController.cancelPendingPulseTimeout();
-            }
-        }
-        if (!isHeadsUp && !mHeadsUpManager.hasNotifications()) {
-            // There are no longer any notifications to show.  We should end the
-            //pulse now.
-            mDozeScrimController.pulseOutNow();
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 0995a00..712953e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -505,7 +505,7 @@
                 v.bind(name, drawable, item.info.id);
             }
             v.setActivated(item.isCurrent);
-            v.setDisabledByAdmin(getController().isDisabledByAdmin(item));
+            v.setDisabledByAdmin(item.isDisabledByAdmin());
             v.setEnabled(item.isSwitchToEnabled);
             UserSwitcherController.setSelectableAlpha(v);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
index 843c232..146b222 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
@@ -19,7 +19,6 @@
 import android.annotation.UserIdInt
 import android.content.Intent
 import android.view.View
-import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
 import com.android.systemui.Dumpable
 import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
 import com.android.systemui.user.data.source.UserRecord
@@ -130,12 +129,6 @@
     /** Whether keyguard is showing. */
     val isKeyguardShowing: Boolean
 
-    /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */
-    fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin?
-
-    /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */
-    fun isDisabledByAdmin(record: UserRecord): Boolean
-
     /** Starts an activity with the given [Intent]. */
     fun startActivity(intent: Intent)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
index 12834f6..1692656 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -17,13 +17,21 @@
 
 package com.android.systemui.statusbar.policy
 
+import android.content.Context
 import android.content.Intent
 import android.view.View
-import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import dagger.Lazy
 import java.io.PrintWriter
 import java.lang.ref.WeakReference
@@ -31,58 +39,76 @@
 import kotlinx.coroutines.flow.Flow
 
 /** Implementation of [UserSwitcherController]. */
+@SysUISingleton
 class UserSwitcherControllerImpl
 @Inject
 constructor(
-    private val flags: FeatureFlags,
+    @Application private val applicationContext: Context,
+    flags: FeatureFlags,
     @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
+    private val userInteractorLazy: Lazy<UserInteractor>,
+    private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
+    private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
+    private val activityStarter: ActivityStarter,
 ) : UserSwitcherController {
 
-    private val isNewImpl: Boolean
-        get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)
+    private val useInteractor: Boolean =
+        flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
+            !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
     private val _oldImpl: UserSwitcherControllerOldImpl
         get() = oldImpl.get()
+    private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
+    private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
+    private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
 
-    private fun notYetImplemented(): Nothing {
-        error("Not yet implemented!")
+    private val callbackCompatMap =
+        mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>()
+
+    private fun notSupported(): Nothing {
+        error("Not supported in the new implementation!")
     }
 
     override val users: ArrayList<UserRecord>
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                userInteractor.userRecords.value
             } else {
                 _oldImpl.users
             }
 
     override val isSimpleUserSwitcher: Boolean
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                userInteractor.isSimpleUserSwitcher
             } else {
                 _oldImpl.isSimpleUserSwitcher
             }
 
     override fun init(view: View) {
-        if (isNewImpl) {
-            notYetImplemented()
-        } else {
+        if (!useInteractor) {
             _oldImpl.init(view)
         }
     }
 
     override val currentUserRecord: UserRecord?
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                userInteractor.selectedUserRecord.value
             } else {
                 _oldImpl.currentUserRecord
             }
 
     override val currentUserName: String?
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                currentUserRecord?.let {
+                    LegacyUserUiHelper.getUserRecordName(
+                        context = applicationContext,
+                        record = it,
+                        isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
+                        isGuestUserResetting = userInteractor.isGuestUserResetting,
+                    )
+                }
             } else {
                 _oldImpl.currentUserName
             }
@@ -91,8 +117,8 @@
         userId: Int,
         dialogShower: UserSwitchDialogController.DialogShower?
     ) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            userInteractor.selectUser(userId)
         } else {
             _oldImpl.onUserSelected(userId, dialogShower)
         }
@@ -100,24 +126,24 @@
 
     override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                notSupported()
             } else {
                 _oldImpl.isAddUsersFromLockScreenEnabled
             }
 
     override val isGuestUserAutoCreated: Boolean
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                userInteractor.isGuestUserAutoCreated
             } else {
                 _oldImpl.isGuestUserAutoCreated
             }
 
     override val isGuestUserResetting: Boolean
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                userInteractor.isGuestUserResetting
             } else {
                 _oldImpl.isGuestUserResetting
             }
@@ -125,40 +151,48 @@
     override fun createAndSwitchToGuestUser(
         dialogShower: UserSwitchDialogController.DialogShower?,
     ) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            notSupported()
         } else {
             _oldImpl.createAndSwitchToGuestUser(dialogShower)
         }
     }
 
     override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            notSupported()
         } else {
             _oldImpl.showAddUserDialog(dialogShower)
         }
     }
 
     override fun startSupervisedUserActivity() {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            notSupported()
         } else {
             _oldImpl.startSupervisedUserActivity()
         }
     }
 
     override fun onDensityOrFontScaleChanged() {
-        if (isNewImpl) {
-            notYetImplemented()
-        } else {
+        if (!useInteractor) {
             _oldImpl.onDensityOrFontScaleChanged()
         }
     }
 
     override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            userInteractor.addCallback(
+                object : UserInteractor.UserCallback {
+                    override fun isEvictable(): Boolean {
+                        return adapter.get() == null
+                    }
+
+                    override fun onUserStateChanged() {
+                        adapter.get()?.notifyDataSetChanged()
+                    }
+                }
+            )
         } else {
             _oldImpl.addAdapter(adapter)
         }
@@ -168,16 +202,23 @@
         record: UserRecord,
         dialogShower: UserSwitchDialogController.DialogShower?,
     ) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            if (LegacyUserDataHelper.isUser(record)) {
+                userInteractor.selectUser(record.resolveId())
+            } else {
+                userInteractor.executeAction(LegacyUserDataHelper.toUserActionModel(record))
+            }
         } else {
             _oldImpl.onUserListItemClicked(record, dialogShower)
         }
     }
 
     override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            userInteractor.removeGuestUser(
+                guestUserId = guestUserId,
+                targetUserId = targetUserId,
+            )
         } else {
             _oldImpl.removeGuestUser(guestUserId, targetUserId)
         }
@@ -188,16 +229,16 @@
         targetUserId: Int,
         forceRemoveGuestOnExit: Boolean
     ) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
         } else {
             _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
         }
     }
 
     override fun schedulePostBootGuestCreation() {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            guestUserInteractor.onDeviceBootCompleted()
         } else {
             _oldImpl.schedulePostBootGuestCreation()
         }
@@ -205,63 +246,57 @@
 
     override val isKeyguardShowing: Boolean
         get() =
-            if (isNewImpl) {
-                notYetImplemented()
+            if (useInteractor) {
+                keyguardInteractor.isKeyguardShowing()
             } else {
                 _oldImpl.isKeyguardShowing
             }
 
-    override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? {
-        return if (isNewImpl) {
-            notYetImplemented()
-        } else {
-            _oldImpl.getEnforcedAdmin(record)
-        }
-    }
-
-    override fun isDisabledByAdmin(record: UserRecord): Boolean {
-        return if (isNewImpl) {
-            notYetImplemented()
-        } else {
-            _oldImpl.isDisabledByAdmin(record)
-        }
-    }
-
     override fun startActivity(intent: Intent) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            activityStarter.startActivity(intent, /* dismissShade= */ false)
         } else {
             _oldImpl.startActivity(intent)
         }
     }
 
     override fun refreshUsers(forcePictureLoadForId: Int) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            userInteractor.refreshUsers()
         } else {
             _oldImpl.refreshUsers(forcePictureLoadForId)
         }
     }
 
     override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            val interactorCallback =
+                object : UserInteractor.UserCallback {
+                    override fun onUserStateChanged() {
+                        callback.onUserSwitched()
+                    }
+                }
+            callbackCompatMap[callback] = interactorCallback
+            userInteractor.addCallback(interactorCallback)
         } else {
             _oldImpl.addUserSwitchCallback(callback)
         }
     }
 
     override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            val interactorCallback = callbackCompatMap.remove(callback)
+            if (interactorCallback != null) {
+                userInteractor.removeCallback(interactorCallback)
+            }
         } else {
             _oldImpl.removeUserSwitchCallback(callback)
         }
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        if (isNewImpl) {
-            notYetImplemented()
+        if (useInteractor) {
+            userInteractor.dump(pw)
         } else {
             _oldImpl.dump(pw, args)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index d365aa6..46d2f3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -17,17 +17,13 @@
 
 import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
 
-import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-
 import android.annotation.UserIdInt;
-import android.app.ActivityManager;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.UserInfo;
@@ -40,7 +36,6 @@
 import android.provider.Settings;
 import android.telephony.TelephonyCallback;
 import android.text.TextUtils;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -49,17 +44,14 @@
 import android.widget.Toast;
 
 import androidx.annotation.Nullable;
-import androidx.collection.SimpleArrayMap;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.util.LatencyTracker;
-import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.users.UserCreatingDialog;
 import com.android.systemui.GuestResetOrExitSessionReceiver;
 import com.android.systemui.GuestResumeSessionReceiver;
-import com.android.systemui.R;
 import com.android.systemui.SystemUISecondaryUserService;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
@@ -75,10 +67,12 @@
 import com.android.systemui.qs.QSUserSwitcherEvent;
 import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.user.CreateUserActivity;
 import com.android.systemui.user.data.source.UserRecord;
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper;
+import com.android.systemui.user.shared.model.UserActionModel;
+import com.android.systemui.user.ui.dialog.AddUserDialog;
+import com.android.systemui.user.ui.dialog.ExitGuestDialog;
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.settings.SecureSettings;
 
@@ -139,9 +133,6 @@
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final LatencyTracker mLatencyTracker;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
-    private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord =
-            new SimpleArrayMap<>();
-    private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>();
 
     private ArrayList<UserRecord> mUsers = new ArrayList<>();
     @VisibleForTesting
@@ -334,7 +325,6 @@
 
             for (UserInfo info : infos) {
                 boolean isCurrent = currentId == info.id;
-                boolean switchToEnabled = canSwitchUsers || isCurrent;
                 if (!mUserSwitcherEnabled && !info.isPrimary()) {
                     continue;
                 }
@@ -343,25 +333,22 @@
                     if (info.isGuest()) {
                         // Tapping guest icon triggers remove and a user switch therefore
                         // the icon shouldn't be enabled even if the user is current
-                        guestRecord = new UserRecord(info, null /* picture */,
-                                true /* isGuest */, isCurrent, false /* isAddUser */,
-                                false /* isRestricted */, canSwitchUsers,
-                                false /* isAddSupervisedUser */);
+                        guestRecord = LegacyUserDataHelper.createRecord(
+                                mContext,
+                                mUserManager,
+                                null /* picture */,
+                                info,
+                                isCurrent,
+                                canSwitchUsers);
                     } else if (info.supportsSwitchToByUser()) {
-                        Bitmap picture = bitmaps.get(info.id);
-                        if (picture == null) {
-                            picture = mUserManager.getUserIcon(info.id);
-
-                            if (picture != null) {
-                                int avatarSize = mContext.getResources()
-                                        .getDimensionPixelSize(R.dimen.max_avatar_size);
-                                picture = Bitmap.createScaledBitmap(
-                                        picture, avatarSize, avatarSize, true);
-                            }
-                        }
-                        records.add(new UserRecord(info, picture, false /* isGuest */,
-                                isCurrent, false /* isAddUser */, false /* isRestricted */,
-                                switchToEnabled, false /* isAddSupervisedUser */));
+                        records.add(
+                                LegacyUserDataHelper.createRecord(
+                                        mContext,
+                                        mUserManager,
+                                        bitmaps.get(info.id),
+                                        info,
+                                        isCurrent,
+                                        canSwitchUsers));
                     }
                 }
             }
@@ -372,18 +359,20 @@
                     // we will just use it as an indicator for "Resetting guest...".
                     // Otherwise, default to canSwitchUsers.
                     boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
-                    guestRecord = new UserRecord(null /* info */, null /* picture */,
-                            true /* isGuest */, false /* isCurrent */,
-                            false /* isAddUser */, false /* isRestricted */,
-                            isSwitchToGuestEnabled, false /* isAddSupervisedUser */);
-                    checkIfAddUserDisallowedByAdminOnly(guestRecord);
+                    guestRecord = LegacyUserDataHelper.createRecord(
+                            mContext,
+                            currentId,
+                            UserActionModel.ENTER_GUEST_MODE,
+                            false /* isRestricted */,
+                            isSwitchToGuestEnabled);
                     records.add(guestRecord);
                 } else if (canCreateGuest(guestRecord != null)) {
-                    guestRecord = new UserRecord(null /* info */, null /* picture */,
-                            true /* isGuest */, false /* isCurrent */,
-                            false /* isAddUser */, createIsRestricted(), canSwitchUsers,
-                            false /* isAddSupervisedUser */);
-                    checkIfAddUserDisallowedByAdminOnly(guestRecord);
+                    guestRecord = LegacyUserDataHelper.createRecord(
+                            mContext,
+                            currentId,
+                            UserActionModel.ENTER_GUEST_MODE,
+                            false /* isRestricted */,
+                            canSwitchUsers);
                     records.add(guestRecord);
                 }
             } else {
@@ -391,20 +380,23 @@
             }
 
             if (canCreateUser()) {
-                UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
-                        false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
-                        createIsRestricted(), canSwitchUsers,
-                        false /* isAddSupervisedUser */);
-                checkIfAddUserDisallowedByAdminOnly(addUserRecord);
-                records.add(addUserRecord);
+                final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+                        mContext,
+                        currentId,
+                        UserActionModel.ADD_USER,
+                        createIsRestricted(),
+                        canSwitchUsers);
+                records.add(userRecord);
             }
 
             if (canCreateSupervisedUser()) {
-                UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
-                        false /* isGuest */, false /* isCurrent */, false /* isAddUser */,
-                        createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */);
-                checkIfAddUserDisallowedByAdminOnly(addUserRecord);
-                records.add(addUserRecord);
+                final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+                        mContext,
+                        currentId,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        createIsRestricted(),
+                        canSwitchUsers);
+                records.add(userRecord);
             }
 
             mUiExecutor.execute(() -> {
@@ -591,12 +583,23 @@
         showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
     }
 
-    private void showExitGuestDialog(int id, boolean isGuestEphemeral,
-                        int targetId, DialogShower dialogShower) {
+    private void showExitGuestDialog(
+            int id,
+            boolean isGuestEphemeral,
+            int targetId,
+            DialogShower dialogShower) {
         if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
             mExitGuestDialog.cancel();
         }
-        mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId);
+        mExitGuestDialog = new ExitGuestDialog(
+                mContext,
+                id,
+                isGuestEphemeral,
+                targetId,
+                mKeyguardStateController.isShowing(),
+                mFalsingManager,
+                mDialogLaunchAnimator,
+                this::exitGuestUser);
         if (dialogShower != null) {
             dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
                     InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
@@ -622,7 +625,15 @@
         if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
             mAddUserDialog.cancel();
         }
-        mAddUserDialog = new AddUserDialog(mContext);
+        final UserInfo currentUser = mUserTracker.getUserInfo();
+        mAddUserDialog = new AddUserDialog(
+                mContext,
+                currentUser.getUserHandle(),
+                mKeyguardStateController.isShowing(),
+                /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(),
+                mFalsingManager,
+                mBroadcastSender,
+                mDialogLaunchAnimator);
         if (dialogShower != null) {
             dialogShower.showDialog(mAddUserDialog,
                     new DialogCuj(
@@ -964,30 +975,6 @@
         return mKeyguardStateController.isShowing();
     }
 
-    @Override
-    @Nullable
-    public EnforcedAdmin getEnforcedAdmin(UserRecord record) {
-        return mEnforcedAdminByUserRecord.get(record);
-    }
-
-    @Override
-    public boolean isDisabledByAdmin(UserRecord record) {
-        return mDisabledByAdmin.contains(record);
-    }
-
-    private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
-        EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
-                UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
-        if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
-                UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) {
-            mDisabledByAdmin.add(record);
-            mEnforcedAdminByUserRecord.put(record, admin);
-        } else {
-            mDisabledByAdmin.remove(record);
-            mEnforcedAdminByUserRecord.put(record, null);
-        }
-    }
-
     private boolean shouldUseSimpleUserSwitcher() {
         int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
@@ -1052,133 +1039,4 @@
                     }
                 }
             };
-
-
-    private final class ExitGuestDialog extends SystemUIDialog implements
-            DialogInterface.OnClickListener {
-
-        private final int mGuestId;
-        private final int mTargetId;
-        private final boolean mIsGuestEphemeral;
-
-        ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
-                    int targetId) {
-            super(context);
-            if (isGuestEphemeral) {
-                setTitle(context.getString(
-                            com.android.settingslib.R.string.guest_exit_dialog_title));
-                setMessage(context.getString(
-                            com.android.settingslib.R.string.guest_exit_dialog_message));
-                setButton(DialogInterface.BUTTON_NEUTRAL,
-                        context.getString(android.R.string.cancel), this);
-                setButton(DialogInterface.BUTTON_POSITIVE,
-                        context.getString(
-                            com.android.settingslib.R.string.guest_exit_dialog_button), this);
-            } else {
-                setTitle(context.getString(
-                            com.android.settingslib
-                                .R.string.guest_exit_dialog_title_non_ephemeral));
-                setMessage(context.getString(
-                            com.android.settingslib
-                                .R.string.guest_exit_dialog_message_non_ephemeral));
-                setButton(DialogInterface.BUTTON_NEUTRAL,
-                        context.getString(android.R.string.cancel), this);
-                setButton(DialogInterface.BUTTON_NEGATIVE,
-                        context.getString(
-                            com.android.settingslib.R.string.guest_exit_clear_data_button),
-                        this);
-                setButton(DialogInterface.BUTTON_POSITIVE,
-                        context.getString(
-                            com.android.settingslib.R.string.guest_exit_save_data_button),
-                        this);
-            }
-            SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
-            setCanceledOnTouchOutside(false);
-            mGuestId = guestId;
-            mTargetId = targetId;
-            mIsGuestEphemeral = isGuestEphemeral;
-        }
-
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
-                    : FalsingManager.HIGH_PENALTY;
-            if (mFalsingManager.isFalseTap(penalty)) {
-                return;
-            }
-            if (mIsGuestEphemeral) {
-                if (which == DialogInterface.BUTTON_POSITIVE) {
-                    mDialogLaunchAnimator.dismissStack(this);
-                    // Ephemeral guest: exit guest, guest is removed by the system
-                    // on exit, since its marked ephemeral
-                    exitGuestUser(mGuestId, mTargetId, false);
-                } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-                    // Cancel clicked, do nothing
-                    cancel();
-                }
-            } else {
-                if (which == DialogInterface.BUTTON_POSITIVE) {
-                    mDialogLaunchAnimator.dismissStack(this);
-                    // Non-ephemeral guest: exit guest, guest is not removed by the system
-                    // on exit, since its marked non-ephemeral
-                    exitGuestUser(mGuestId, mTargetId, false);
-                } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-                    mDialogLaunchAnimator.dismissStack(this);
-                    // Non-ephemeral guest: remove guest and then exit
-                    exitGuestUser(mGuestId, mTargetId, true);
-                } else if (which == DialogInterface.BUTTON_NEUTRAL) {
-                    // Cancel clicked, do nothing
-                    cancel();
-                }
-            }
-        }
-    }
-
-    @VisibleForTesting
-    final class AddUserDialog extends SystemUIDialog implements
-            DialogInterface.OnClickListener {
-
-        AddUserDialog(Context context) {
-            super(context);
-
-            setTitle(com.android.settingslib.R.string.user_add_user_title);
-            String message = context.getString(
-                                com.android.settingslib.R.string.user_add_user_message_short);
-            UserInfo currentUser = mUserTracker.getUserInfo();
-            if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) {
-                message += context.getString(R.string.user_add_user_message_guest_remove);
-            }
-            setMessage(message);
-            setButton(DialogInterface.BUTTON_NEUTRAL,
-                    context.getString(android.R.string.cancel), this);
-            setButton(DialogInterface.BUTTON_POSITIVE,
-                    context.getString(android.R.string.ok), this);
-            SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
-        }
-
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
-                    : FalsingManager.MODERATE_PENALTY;
-            if (mFalsingManager.isFalseTap(penalty)) {
-                return;
-            }
-            if (which == BUTTON_NEUTRAL) {
-                cancel();
-            } else {
-                mDialogLaunchAnimator.dismissStack(this);
-                if (ActivityManager.isUserAMonkey()) {
-                    return;
-                }
-                // Use broadcast instead of ShadeController, as this dialog may have started in
-                // another process and normal dagger bindings are not available
-                mBroadcastSender.sendBroadcastAsUser(
-                        new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT);
-                getContext().startActivityAsUser(
-                        CreateUserActivity.createIntentForStart(getContext()),
-                        mUserTracker.getUserHandle());
-            }
-        }
-    }
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
new file mode 100644
index 0000000..9c38dc0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.telephony.data.repository
+
+import android.telephony.Annotation
+import android.telephony.TelephonyCallback
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.TelephonyListenerManager
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface for classes that encapsulate _some_ telephony-related state. */
+interface TelephonyRepository {
+    /** The state of the current call. */
+    @Annotation.CallState val callState: Flow<Int>
+}
+
+/**
+ * NOTE: This repository tracks only telephony-related state regarding the default mobile
+ * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a
+ * per-subscription basis and thus will always be tracking telephony information regarding
+ * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager`
+ * for more documentation.
+ */
+@SysUISingleton
+class TelephonyRepositoryImpl
+@Inject
+constructor(
+    private val manager: TelephonyListenerManager,
+) : TelephonyRepository {
+    @Annotation.CallState
+    override val callState: Flow<Int> = conflatedCallbackFlow {
+        val listener = TelephonyCallback.CallStateListener { state -> trySend(state) }
+
+        manager.addCallStateListener(listener)
+
+        awaitClose { manager.removeCallStateListener(listener) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
new file mode 100644
index 0000000..630fbf2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.telephony.data.repository
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface TelephonyRepositoryModule {
+    @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
new file mode 100644
index 0000000..86ca33d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.telephony.domain.interactor
+
+import android.telephony.Annotation
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.data.repository.TelephonyRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Hosts business logic related to telephony. */
+@SysUISingleton
+class TelephonyInteractor
+@Inject
+constructor(
+    repository: TelephonyRepository,
+) {
+    @Annotation.CallState val callState: Flow<Int> = repository.callState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index 5b522dc..0c72b78 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -20,6 +20,7 @@
 
 import com.android.settingslib.users.EditUserInfoController;
 import com.android.systemui.user.data.repository.UserRepositoryModule;
+import com.android.systemui.user.ui.dialog.UserDialogModule;
 
 import dagger.Binds;
 import dagger.Module;
@@ -32,6 +33,7 @@
  */
 @Module(
         includes = {
+                UserDialogModule.class,
                 UserRepositoryModule.class,
         }
 )
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
new file mode 100644
index 0000000..4fd55c0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.user.data.model
+
+/** Encapsulates the state of settings related to user switching. */
+data class UserSwitcherSettingsModel(
+    val isSimpleUserSwitcher: Boolean = false,
+    val isAddUsersFromLockscreen: Boolean = false,
+    val isUserSwitcherEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 0356388..3014f39 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,9 +18,13 @@
 package com.android.systemui.user.data.repository
 
 import android.content.Context
+import android.content.pm.UserInfo
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.Drawable
+import android.os.UserHandle
 import android.os.UserManager
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
 import androidx.appcompat.content.res.AppCompatResources
 import com.android.internal.util.UserIcons
 import com.android.systemui.R
@@ -29,15 +33,36 @@
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.source.UserRecord
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /**
  * Acts as source of truth for user related data.
@@ -55,6 +80,18 @@
     /** List of available user-related actions. */
     val actions: Flow<List<UserActionModel>>
 
+    /** User switcher related settings. */
+    val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
+
+    /** List of all users on the device. */
+    val userInfos: Flow<List<UserInfo>>
+
+    /** [UserInfo] of the currently-selected user. */
+    val selectedUserInfo: Flow<UserInfo>
+
+    /** User ID of the last non-guest selected user. */
+    val lastSelectedNonGuestUserId: Int
+
     /** Whether actions are available even when locked. */
     val isActionableWhenLocked: Flow<Boolean>
 
@@ -62,7 +99,23 @@
     val isGuestUserAutoCreated: Boolean
 
     /** Whether the guest user is currently being reset. */
-    val isGuestUserResetting: Boolean
+    var isGuestUserResetting: Boolean
+
+    /** Whether we've scheduled the creation of a guest user. */
+    val isGuestUserCreationScheduled: AtomicBoolean
+
+    /** The user of the secondary service. */
+    var secondaryUserId: Int
+
+    /** Whether refresh users should be paused. */
+    var isRefreshUsersPaused: Boolean
+
+    /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
+    fun refreshUsers()
+
+    fun getSelectedUserInfo(): UserInfo
+
+    fun isSimpleUserSwitcher(): Boolean
 }
 
 @SysUISingleton
@@ -71,9 +124,31 @@
 constructor(
     @Application private val appContext: Context,
     private val manager: UserManager,
-    controller: UserSwitcherController,
+    private val controller: UserSwitcherController,
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val globalSettings: GlobalSettings,
+    private val tracker: UserTracker,
+    private val featureFlags: FeatureFlags,
 ) : UserRepository {
 
+    private val isNewImpl: Boolean
+        get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+    private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
+    override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+        _userSwitcherSettings.asStateFlow().filterNotNull()
+
+    private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
+    override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
+
+    private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+    override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+    override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+        private set
+
     private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
         fun send() {
             trySendWithFailureLogging(
@@ -99,11 +174,148 @@
     override val actions: Flow<List<UserActionModel>> =
         userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
 
-    override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
+    override val isActionableWhenLocked: Flow<Boolean> =
+        if (isNewImpl) {
+            emptyFlow()
+        } else {
+            controller.isAddUsersFromLockScreenEnabled
+        }
 
-    override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
+    override val isGuestUserAutoCreated: Boolean =
+        if (isNewImpl) {
+            appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
+        } else {
+            controller.isGuestUserAutoCreated
+        }
 
-    override val isGuestUserResetting: Boolean = controller.isGuestUserResetting
+    private var _isGuestUserResetting: Boolean = false
+    override var isGuestUserResetting: Boolean =
+        if (isNewImpl) {
+            _isGuestUserResetting
+        } else {
+            controller.isGuestUserResetting
+        }
+        set(value) =
+            if (isNewImpl) {
+                _isGuestUserResetting = value
+            } else {
+                error("Not supported in the old implementation!")
+            }
+
+    override val isGuestUserCreationScheduled = AtomicBoolean()
+
+    override var secondaryUserId: Int = UserHandle.USER_NULL
+
+    override var isRefreshUsersPaused: Boolean = false
+
+    init {
+        if (isNewImpl) {
+            observeSelectedUser()
+            observeUserSettings()
+        }
+    }
+
+    override fun refreshUsers() {
+        applicationScope.launch {
+            val result = withContext(backgroundDispatcher) { manager.aliveUsers }
+
+            if (result != null) {
+                _userInfos.value = result
+            }
+        }
+    }
+
+    override fun getSelectedUserInfo(): UserInfo {
+        return checkNotNull(_selectedUserInfo.value)
+    }
+
+    override fun isSimpleUserSwitcher(): Boolean {
+        return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher)
+    }
+
+    private fun observeSelectedUser() {
+        conflatedCallbackFlow {
+                fun send() {
+                    trySendWithFailureLogging(tracker.userInfo, TAG)
+                }
+
+                val callback =
+                    object : UserTracker.Callback {
+                        override fun onUserChanged(newUser: Int, userContext: Context) {
+                            send()
+                        }
+                    }
+
+                tracker.addCallback(callback, mainDispatcher.asExecutor())
+                send()
+
+                awaitClose { tracker.removeCallback(callback) }
+            }
+            .onEach {
+                if (!it.isGuest) {
+                    lastSelectedNonGuestUserId = it.id
+                }
+
+                _selectedUserInfo.value = it
+            }
+            .launchIn(applicationScope)
+    }
+
+    private fun observeUserSettings() {
+        globalSettings
+            .observerFlow(
+                names =
+                    arrayOf(
+                        SETTING_SIMPLE_USER_SWITCHER,
+                        Settings.Global.ADD_USERS_WHEN_LOCKED,
+                        Settings.Global.USER_SWITCHER_ENABLED,
+                    ),
+                userId = UserHandle.USER_SYSTEM,
+            )
+            .onStart { emit(Unit) } // Forces an initial update.
+            .map { getSettings() }
+            .onEach { _userSwitcherSettings.value = it }
+            .launchIn(applicationScope)
+    }
+
+    private suspend fun getSettings(): UserSwitcherSettingsModel {
+        return withContext(backgroundDispatcher) {
+            val isSimpleUserSwitcher =
+                globalSettings.getIntForUser(
+                    SETTING_SIMPLE_USER_SWITCHER,
+                    if (
+                        appContext.resources.getBoolean(
+                            com.android.internal.R.bool.config_expandLockScreenUserSwitcher
+                        )
+                    ) {
+                        1
+                    } else {
+                        0
+                    },
+                    UserHandle.USER_SYSTEM,
+                ) != 0
+
+            val isAddUsersFromLockscreen =
+                globalSettings.getIntForUser(
+                    Settings.Global.ADD_USERS_WHEN_LOCKED,
+                    0,
+                    UserHandle.USER_SYSTEM,
+                ) != 0
+
+            val isUserSwitcherEnabled =
+                globalSettings.getIntForUser(
+                    Settings.Global.USER_SWITCHER_ENABLED,
+                    0,
+                    UserHandle.USER_SYSTEM,
+                ) != 0
+
+            UserSwitcherSettingsModel(
+                isSimpleUserSwitcher = isSimpleUserSwitcher,
+                isAddUsersFromLockscreen = isAddUsersFromLockscreen,
+                isUserSwitcherEnabled = isUserSwitcherEnabled,
+            )
+        }
+    }
 
     private fun UserRecord.isUser(): Boolean {
         return when {
@@ -125,6 +337,7 @@
             image = getUserImage(this),
             isSelected = isCurrent,
             isSelectable = isSwitchToEnabled || isGuest,
+            isGuest = isGuest,
         )
     }
 
@@ -162,5 +375,6 @@
 
     companion object {
         private const val TAG = "UserRepository"
+        @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index cf6da9a..9370286 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -19,6 +19,7 @@
 import android.content.pm.UserInfo
 import android.graphics.Bitmap
 import android.os.UserHandle
+import com.android.settingslib.RestrictedLockUtils
 
 /** Encapsulates raw data for a user or an option item related to managing users on the device. */
 data class UserRecord(
@@ -41,6 +42,11 @@
     @JvmField val isSwitchToEnabled: Boolean = false,
     /** Whether this record represents an option to add another supervised user to the device. */
     @JvmField val isAddSupervisedUser: Boolean = false,
+    /**
+     * An enforcing admin, if the user action represented by this record is disabled by the admin.
+     * If not disabled, this is `null`.
+     */
+    @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
 ) {
     /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
     fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
@@ -59,6 +65,14 @@
         }
     }
 
+    /**
+     * Returns `true` if the user action represented by this record has been disabled by an admin;
+     * `false` otherwise.
+     */
+    fun isDisabledByAdmin(): Boolean {
+        return enforcedAdmin != null
+    }
+
     companion object {
         @JvmStatic
         fun createForGuest(): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
new file mode 100644
index 0000000..07e5cf9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.user.domain.interactor
+
+import android.annotation.UserIdInt
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManagerGlobal
+import android.widget.Toast
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/** Encapsulates business logic to interact with guest user data and systems. */
+@SysUISingleton
+class GuestUserInteractor
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val manager: UserManager,
+    private val repository: UserRepository,
+    private val deviceProvisionedController: DeviceProvisionedController,
+    private val devicePolicyManager: DevicePolicyManager,
+    private val refreshUsersScheduler: RefreshUsersScheduler,
+    private val uiEventLogger: UiEventLogger,
+) {
+    /** Whether the device is configured to always have a guest user available. */
+    val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+
+    /** Whether the guest user is currently being reset. */
+    val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+
+    /** Notifies that the device has finished booting. */
+    fun onDeviceBootCompleted() {
+        applicationScope.launch {
+            if (isDeviceAllowedToAddGuest()) {
+                guaranteePresent()
+                return@launch
+            }
+
+            suspendCancellableCoroutine<Unit> { continuation ->
+                val callback =
+                    object : DeviceProvisionedController.DeviceProvisionedListener {
+                        override fun onDeviceProvisionedChanged() {
+                            continuation.resumeWith(Result.success(Unit))
+                            deviceProvisionedController.removeCallback(this)
+                        }
+                    }
+
+                deviceProvisionedController.addCallback(callback)
+            }
+
+            if (isDeviceAllowedToAddGuest()) {
+                guaranteePresent()
+            }
+        }
+    }
+
+    /** Creates a guest user and switches to it. */
+    fun createAndSwitchTo(
+        showDialog: (ShowDialogRequestModel) -> Unit,
+        dismissDialog: () -> Unit,
+        selectUser: (userId: Int) -> Unit,
+    ) {
+        applicationScope.launch {
+            val newGuestUserId = create(showDialog, dismissDialog)
+            if (newGuestUserId != UserHandle.USER_NULL) {
+                selectUser(newGuestUserId)
+            }
+        }
+    }
+
+    /** Exits the guest user, switching back to the last non-guest user or to the default user. */
+    fun exit(
+        @UserIdInt guestUserId: Int,
+        @UserIdInt targetUserId: Int,
+        forceRemoveGuestOnExit: Boolean,
+        showDialog: (ShowDialogRequestModel) -> Unit,
+        dismissDialog: () -> Unit,
+        switchUser: (userId: Int) -> Unit,
+    ) {
+        val currentUserInfo = repository.getSelectedUserInfo()
+        if (currentUserInfo.id != guestUserId) {
+            Log.w(
+                TAG,
+                "User requesting to start a new session ($guestUserId) is not current user" +
+                    " (${currentUserInfo.id})"
+            )
+            return
+        }
+
+        if (!currentUserInfo.isGuest) {
+            Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+            return
+        }
+
+        applicationScope.launch {
+            var newUserId = UserHandle.USER_SYSTEM
+            if (targetUserId == UserHandle.USER_NULL) {
+                // When a target user is not specified switch to last non guest user:
+                val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
+                if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
+                    val info =
+                        withContext(backgroundDispatcher) {
+                            manager.getUserInfo(lastSelectedNonGuestUserHandle)
+                        }
+                    if (info != null && info.isEnabled && info.supportsSwitchToByUser()) {
+                        newUserId = info.id
+                    }
+                }
+            } else {
+                newUserId = targetUserId
+            }
+
+            if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
+                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
+                remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
+            } else {
+                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
+                switchUser(newUserId)
+            }
+        }
+    }
+
+    /**
+     * Guarantees that the guest user is present on the device, creating it if needed and if allowed
+     * to.
+     */
+    suspend fun guaranteePresent() {
+        if (!isDeviceAllowedToAddGuest()) {
+            return
+        }
+
+        val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
+        if (guestUser == null) {
+            scheduleCreation()
+        }
+    }
+
+    /** Removes the guest user from the device. */
+    suspend fun remove(
+        @UserIdInt guestUserId: Int,
+        @UserIdInt targetUserId: Int,
+        showDialog: (ShowDialogRequestModel) -> Unit,
+        dismissDialog: () -> Unit,
+        switchUser: (userId: Int) -> Unit,
+    ) {
+        val currentUser: UserInfo = repository.getSelectedUserInfo()
+        if (currentUser.id != guestUserId) {
+            Log.w(
+                TAG,
+                "User requesting to start a new session ($guestUserId) is not current user" +
+                    " ($currentUser.id)"
+            )
+            return
+        }
+
+        if (!currentUser.isGuest) {
+            Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+            return
+        }
+
+        val marked =
+            withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
+        if (!marked) {
+            Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
+            return
+        }
+
+        if (targetUserId == UserHandle.USER_NULL) {
+            // Create a new guest in the foreground, and then immediately switch to it
+            val newGuestId = create(showDialog, dismissDialog)
+            if (newGuestId == UserHandle.USER_NULL) {
+                Log.e(TAG, "Could not create new guest, switching back to system user")
+                switchUser(UserHandle.USER_SYSTEM)
+                withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+                try {
+                    WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
+                } catch (e: RemoteException) {
+                    Log.e(
+                        TAG,
+                        "Couldn't remove guest because ActivityManager or WindowManager is dead"
+                    )
+                }
+                return
+            }
+
+            switchUser(newGuestId)
+
+            withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+        } else {
+            if (repository.isGuestUserAutoCreated) {
+                repository.isGuestUserResetting = true
+            }
+            switchUser(targetUserId)
+            manager.removeUser(currentUser.id)
+        }
+    }
+
+    /**
+     * Creates the guest user and adds it to the device.
+     *
+     * @param showDialog A function to invoke to show a dialog.
+     * @param dismissDialog A function to invoke to dismiss a dialog.
+     * @return The user ID of the newly-created guest user.
+     */
+    private suspend fun create(
+        showDialog: (ShowDialogRequestModel) -> Unit,
+        dismissDialog: () -> Unit,
+    ): Int {
+        return withContext(mainDispatcher) {
+            showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+            val guestUserId = createInBackground()
+            dismissDialog()
+            if (guestUserId != UserHandle.USER_NULL) {
+                uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
+            } else {
+                Toast.makeText(
+                        applicationContext,
+                        com.android.settingslib.R.string.add_guest_failed,
+                        Toast.LENGTH_SHORT,
+                    )
+                    .show()
+            }
+
+            guestUserId
+        }
+    }
+
+    /** Schedules the creation of the guest user. */
+    private suspend fun scheduleCreation() {
+        if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
+            return
+        }
+
+        withContext(backgroundDispatcher) {
+            val newGuestUserId = createInBackground()
+            repository.isGuestUserCreationScheduled.set(false)
+            repository.isGuestUserResetting = false
+            if (newGuestUserId == UserHandle.USER_NULL) {
+                Log.w(TAG, "Could not create new guest while exiting existing guest")
+                // Refresh users so that we still display "Guest" if
+                // config_guestUserAutoCreated=true
+                refreshUsersScheduler.refreshIfNotPaused()
+            }
+        }
+    }
+
+    /**
+     * Creates a guest user and return its multi-user user ID.
+     *
+     * This method does not check if a guest already exists before it makes a call to [UserManager]
+     * to create a new one.
+     *
+     * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
+     * the guest couldn't be created.
+     */
+    @UserIdInt
+    private suspend fun createInBackground(): Int {
+        return withContext(backgroundDispatcher) {
+            try {
+                val guestUser = manager.createGuest(applicationContext)
+                if (guestUser != null) {
+                    guestUser.id
+                } else {
+                    Log.e(
+                        TAG,
+                        "Couldn't create guest, most likely because there already exists one!"
+                    )
+                    UserHandle.USER_NULL
+                }
+            } catch (e: UserManager.UserOperationException) {
+                Log.e(TAG, "Couldn't create guest user!", e)
+                UserHandle.USER_NULL
+            }
+        }
+    }
+
+    private fun isDeviceAllowedToAddGuest(): Boolean {
+        return deviceProvisionedController.isDeviceProvisioned &&
+            !devicePolicyManager.isDeviceManaged
+    }
+
+    companion object {
+        private const val TAG = "GuestUserInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
new file mode 100644
index 0000000..8f36821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.user.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */
+@SysUISingleton
+class RefreshUsersScheduler
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    private val repository: UserRepository,
+) {
+    private var scheduledUnpauseJob: Job? = null
+    private var isPaused = false
+
+    fun pause() {
+        applicationScope.launch(mainDispatcher) {
+            isPaused = true
+            scheduledUnpauseJob?.cancel()
+            scheduledUnpauseJob =
+                applicationScope.launch {
+                    delay(PAUSE_REFRESH_USERS_TIMEOUT_MS)
+                    unpauseAndRefresh()
+                }
+        }
+    }
+
+    fun unpauseAndRefresh() {
+        applicationScope.launch(mainDispatcher) {
+            isPaused = false
+            refreshIfNotPaused()
+        }
+    }
+
+    fun refreshIfNotPaused() {
+        applicationScope.launch(mainDispatcher) {
+            if (isPaused) {
+                return@launch
+            }
+
+            repository.refreshUsers()
+        }
+    }
+
+    companion object {
+        private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
new file mode 100644
index 0000000..1b4746a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.user.domain.interactor
+
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.systemui.user.data.repository.UserRepository
+
+/** Utilities related to user management actions. */
+object UserActionsUtil {
+
+    /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */
+    fun canCreateGuest(
+        manager: UserManager,
+        repository: UserRepository,
+        isUserSwitcherEnabled: Boolean,
+        isAddUsersFromLockScreenEnabled: Boolean,
+    ): Boolean {
+        if (!isUserSwitcherEnabled) {
+            return false
+        }
+
+        return currentUserCanCreateUsers(manager, repository) ||
+            anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+    }
+
+    /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */
+    fun canCreateUser(
+        manager: UserManager,
+        repository: UserRepository,
+        isUserSwitcherEnabled: Boolean,
+        isAddUsersFromLockScreenEnabled: Boolean,
+    ): Boolean {
+        if (!isUserSwitcherEnabled) {
+            return false
+        }
+
+        if (
+            !currentUserCanCreateUsers(manager, repository) &&
+                !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+        ) {
+            return false
+        }
+
+        return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY)
+    }
+
+    /**
+     * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise.
+     */
+    fun canCreateSupervisedUser(
+        manager: UserManager,
+        repository: UserRepository,
+        isUserSwitcherEnabled: Boolean,
+        isAddUsersFromLockScreenEnabled: Boolean,
+        supervisedUserPackageName: String?
+    ): Boolean {
+        if (supervisedUserPackageName.isNullOrEmpty()) {
+            return false
+        }
+
+        return canCreateUser(
+            manager,
+            repository,
+            isUserSwitcherEnabled,
+            isAddUsersFromLockScreenEnabled
+        )
+    }
+
+    /**
+     * Returns `true` if the current user is allowed to add users to the device; `false` otherwise.
+     */
+    private fun currentUserCanCreateUsers(
+        manager: UserManager,
+        repository: UserRepository,
+    ): Boolean {
+        val currentUser = repository.getSelectedUserInfo()
+        if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) {
+            return false
+        }
+
+        return systemCanCreateUsers(manager)
+    }
+
+    /** Returns `true` if the system can add users to the device; `false` otherwise. */
+    private fun systemCanCreateUsers(
+        manager: UserManager,
+    ): Boolean {
+        return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM)
+    }
+
+    /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */
+    private fun anyoneCanCreateUsers(
+        manager: UserManager,
+        isAddUsersFromLockScreenEnabled: Boolean,
+    ): Boolean {
+        return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 3c5b969..a84238c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -17,94 +17,725 @@
 
 package com.android.systemui.user.domain.interactor
 
+import android.annotation.SuppressLint
+import android.annotation.UserIdInt
+import android.app.ActivityManager
+import android.content.Context
 import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
 import android.provider.Settings
+import android.util.Log
+import com.android.internal.util.UserIcons
+import com.android.systemui.R
+import com.android.systemui.SystemUISecondaryUserService
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
 import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.kotlin.pairwise
+import java.io.PrintWriter
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
 
 /** Encapsulates business logic to interact with user data and systems. */
 @SysUISingleton
 class UserInteractor
 @Inject
 constructor(
-    repository: UserRepository,
+    @Application private val applicationContext: Context,
+    private val repository: UserRepository,
     private val controller: UserSwitcherController,
     private val activityStarter: ActivityStarter,
-    keyguardInteractor: KeyguardInteractor,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val featureFlags: FeatureFlags,
+    private val manager: UserManager,
+    @Application private val applicationScope: CoroutineScope,
+    telephonyInteractor: TelephonyInteractor,
+    broadcastDispatcher: BroadcastDispatcher,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val activityManager: ActivityManager,
+    private val refreshUsersScheduler: RefreshUsersScheduler,
+    private val guestUserInteractor: GuestUserInteractor,
 ) {
+    /**
+     * Defines interface for classes that can be notified when the state of users on the device is
+     * changed.
+     */
+    interface UserCallback {
+        /** Returns `true` if this callback can be cleaned-up. */
+        fun isEvictable(): Boolean = false
+        /** Notifies that the state of users on the device has changed. */
+        fun onUserStateChanged()
+    }
+
+    private val isNewImpl: Boolean
+        get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+    private val supervisedUserPackageName: String?
+        get() =
+            applicationContext.getString(
+                com.android.internal.R.string.config_supervisedUserCreationPackage
+            )
+
+    private val callbackMutex = Mutex()
+    private val callbacks = mutableSetOf<UserCallback>()
+
     /** List of current on-device users to select from. */
-    val users: Flow<List<UserModel>> = repository.users
+    val users: Flow<List<UserModel>>
+        get() =
+            if (isNewImpl) {
+                combine(
+                    repository.userInfos,
+                    repository.selectedUserInfo,
+                    repository.userSwitcherSettings,
+                ) { userInfos, selectedUserInfo, settings ->
+                    toUserModels(
+                        userInfos = userInfos,
+                        selectedUserId = selectedUserInfo.id,
+                        isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+                    )
+                }
+            } else {
+                repository.users
+            }
 
     /** The currently-selected user. */
-    val selectedUser: Flow<UserModel> = repository.selectedUser
+    val selectedUser: Flow<UserModel>
+        get() =
+            if (isNewImpl) {
+                combine(
+                    repository.selectedUserInfo,
+                    repository.userSwitcherSettings,
+                ) { selectedUserInfo, settings ->
+                    val selectedUserId = selectedUserInfo.id
+                    checkNotNull(
+                        toUserModel(
+                            userInfo = selectedUserInfo,
+                            selectedUserId = selectedUserId,
+                            canSwitchUsers = canSwitchUsers(selectedUserId),
+                            isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+                        )
+                    )
+                }
+            } else {
+                repository.selectedUser
+            }
 
     /** List of user-switcher related actions that are available. */
-    val actions: Flow<List<UserActionModel>> =
-        combine(
-                repository.isActionableWhenLocked,
-                keyguardInteractor.isKeyguardShowing,
-            ) { isActionableWhenLocked, isLocked ->
-                isActionableWhenLocked || !isLocked
-            }
-            .flatMapLatest { isActionable ->
-                if (isActionable) {
-                    repository.actions.map { actions ->
-                        actions +
-                            if (actions.isNotEmpty()) {
-                                // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because
-                                // that's a user
-                                // switcher specific action that is not known to the our data source
-                                // or other
-                                // features.
-                                listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                            } else {
-                                // If no actions, don't add the navigate action.
-                                emptyList()
-                            }
+    val actions: Flow<List<UserActionModel>>
+        get() =
+            if (isNewImpl) {
+                combine(
+                    repository.userInfos,
+                    repository.userSwitcherSettings,
+                    keyguardInteractor.isKeyguardShowing,
+                ) { userInfos, settings, isDeviceLocked ->
+                    buildList {
+                        val hasGuestUser = userInfos.any { it.isGuest }
+                        if (
+                            !hasGuestUser &&
+                                (guestUserInteractor.isGuestUserAutoCreated ||
+                                    UserActionsUtil.canCreateGuest(
+                                        manager,
+                                        repository,
+                                        settings.isUserSwitcherEnabled,
+                                        settings.isAddUsersFromLockscreen,
+                                    ))
+                        ) {
+                            add(UserActionModel.ENTER_GUEST_MODE)
+                        }
+
+                        if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
+                            // The device is locked and our setting to allow actions that add users
+                            // from the lock-screen is not enabled. The guest action from above is
+                            // always allowed, even when the device is locked, but the various "add
+                            // user" actions below are not. We can finish building the list here.
+                            return@buildList
+                        }
+
+                        if (
+                            UserActionsUtil.canCreateUser(
+                                manager,
+                                repository,
+                                settings.isUserSwitcherEnabled,
+                                settings.isAddUsersFromLockscreen,
+                            )
+                        ) {
+                            add(UserActionModel.ADD_USER)
+                        }
+
+                        if (
+                            UserActionsUtil.canCreateSupervisedUser(
+                                manager,
+                                repository,
+                                settings.isUserSwitcherEnabled,
+                                settings.isAddUsersFromLockscreen,
+                                supervisedUserPackageName,
+                            )
+                        ) {
+                            add(UserActionModel.ADD_SUPERVISED_USER)
+                        }
                     }
-                } else {
-                    // If not actionable it means that we're not allowed to show actions when locked
-                    // and we
-                    // are locked. Therefore, we should show no actions.
-                    flowOf(emptyList())
                 }
+            } else {
+                combine(
+                        repository.isActionableWhenLocked,
+                        keyguardInteractor.isKeyguardShowing,
+                    ) { isActionableWhenLocked, isLocked ->
+                        isActionableWhenLocked || !isLocked
+                    }
+                    .flatMapLatest { isActionable ->
+                        if (isActionable) {
+                            repository.actions.map { actions ->
+                                actions +
+                                    if (actions.isNotEmpty()) {
+                                        // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
+                                        // because that's a user switcher specific action that is
+                                        // not known to the our data source or other features.
+                                        listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+                                    } else {
+                                        // If no actions, don't add the navigate action.
+                                        emptyList()
+                                    }
+                            }
+                        } else {
+                            // If not actionable it means that we're not allowed to show actions
+                            // when
+                            // locked and we are locked. Therefore, we should show no actions.
+                            flowOf(emptyList())
+                        }
+                    }
             }
 
+    val userRecords: StateFlow<ArrayList<UserRecord>> =
+        if (isNewImpl) {
+            combine(
+                    repository.userInfos,
+                    repository.selectedUserInfo,
+                    actions,
+                    repository.userSwitcherSettings,
+                ) { userInfos, selectedUserInfo, actionModels, settings ->
+                    ArrayList(
+                        userInfos.map {
+                            toRecord(
+                                userInfo = it,
+                                selectedUserId = selectedUserInfo.id,
+                            )
+                        } +
+                            actionModels.map {
+                                toRecord(
+                                    action = it,
+                                    selectedUserId = selectedUserInfo.id,
+                                    isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
+                                )
+                            }
+                    )
+                }
+                .onEach { notifyCallbacks() }
+                .stateIn(
+                    scope = applicationScope,
+                    started = SharingStarted.Eagerly,
+                    initialValue = ArrayList(),
+                )
+        } else {
+            MutableStateFlow(ArrayList())
+        }
+
+    val selectedUserRecord: StateFlow<UserRecord?> =
+        if (isNewImpl) {
+            repository.selectedUserInfo
+                .map { selectedUserInfo ->
+                    toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
+                }
+                .stateIn(
+                    scope = applicationScope,
+                    started = SharingStarted.Eagerly,
+                    initialValue = null,
+                )
+        } else {
+            MutableStateFlow(null)
+        }
+
     /** Whether the device is configured to always have a guest user available. */
-    val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+    val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
 
     /** Whether the guest user is currently being reset. */
-    val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+    val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting
+
+    private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null)
+    val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow()
+
+    private val _dialogDismissRequests = MutableStateFlow<Unit?>(null)
+    val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
+
+    val isSimpleUserSwitcher: Boolean
+        get() =
+            if (isNewImpl) {
+                repository.isSimpleUserSwitcher()
+            } else {
+                error("Not supported in the old implementation!")
+            }
+
+    init {
+        if (isNewImpl) {
+            refreshUsersScheduler.refreshIfNotPaused()
+            telephonyInteractor.callState
+                .distinctUntilChanged()
+                .onEach { refreshUsersScheduler.refreshIfNotPaused() }
+                .launchIn(applicationScope)
+
+            combine(
+                    broadcastDispatcher.broadcastFlow(
+                        filter =
+                            IntentFilter().apply {
+                                addAction(Intent.ACTION_USER_ADDED)
+                                addAction(Intent.ACTION_USER_REMOVED)
+                                addAction(Intent.ACTION_USER_INFO_CHANGED)
+                                addAction(Intent.ACTION_USER_SWITCHED)
+                                addAction(Intent.ACTION_USER_STOPPED)
+                                addAction(Intent.ACTION_USER_UNLOCKED)
+                            },
+                        user = UserHandle.SYSTEM,
+                        map = { intent, _ -> intent },
+                    ),
+                    repository.selectedUserInfo.pairwise(null),
+                ) { intent, selectedUserChange ->
+                    Pair(intent, selectedUserChange.previousValue)
+                }
+                .onEach { (intent, previousSelectedUser) ->
+                    onBroadcastReceived(intent, previousSelectedUser)
+                }
+                .launchIn(applicationScope)
+        }
+    }
+
+    fun addCallback(callback: UserCallback) {
+        applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } }
+    }
+
+    fun removeCallback(callback: UserCallback) {
+        applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } }
+    }
+
+    fun refreshUsers() {
+        refreshUsersScheduler.refreshIfNotPaused()
+    }
+
+    fun onDialogShown() {
+        _dialogShowRequests.value = null
+    }
+
+    fun onDialogDismissed() {
+        _dialogDismissRequests.value = null
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.println("UserInteractor state:")
+        pw.println("  lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}")
+
+        val users = userRecords.value.filter { it.info != null }
+        pw.println("  userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}")
+        for (i in users.indices) {
+            pw.println("    ${users[i]}")
+        }
+
+        val actions = userRecords.value.filter { it.info == null }
+        pw.println("  actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}")
+        for (i in actions.indices) {
+            pw.println("    ${actions[i]}")
+        }
+
+        pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher")
+        pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated")
+    }
+
+    fun onDeviceBootCompleted() {
+        guestUserInteractor.onDeviceBootCompleted()
+    }
 
     /** Switches to the user with the given user ID. */
     fun selectUser(
-        userId: Int,
+        newlySelectedUserId: Int,
     ) {
-        controller.onUserSelected(userId, /* dialogShower= */ null)
+        if (isNewImpl) {
+            val currentlySelectedUserInfo = repository.getSelectedUserInfo()
+            if (
+                newlySelectedUserId == currentlySelectedUserInfo.id &&
+                    currentlySelectedUserInfo.isGuest
+            ) {
+                // Here when clicking on the currently-selected guest user to leave guest mode
+                // and return to the previously-selected non-guest user.
+                showDialog(
+                    ShowDialogRequestModel.ShowExitGuestDialog(
+                        guestUserId = currentlySelectedUserInfo.id,
+                        targetUserId = repository.lastSelectedNonGuestUserId,
+                        isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+                        isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+                        onExitGuestUser = this::exitGuestUser,
+                    )
+                )
+                return
+            }
+
+            if (currentlySelectedUserInfo.isGuest) {
+                // Here when switching from guest to a non-guest user.
+                showDialog(
+                    ShowDialogRequestModel.ShowExitGuestDialog(
+                        guestUserId = currentlySelectedUserInfo.id,
+                        targetUserId = newlySelectedUserId,
+                        isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+                        isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+                        onExitGuestUser = this::exitGuestUser,
+                    )
+                )
+                return
+            }
+
+            switchUser(newlySelectedUserId)
+        } else {
+            controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null)
+        }
     }
 
     /** Executes the given action. */
     fun executeAction(action: UserActionModel) {
-        when (action) {
-            UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
-            UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
-            UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
-            UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
-                activityStarter.startActivity(
-                    Intent(Settings.ACTION_USER_SETTINGS),
-                    /* dismissShade= */ false,
-                )
+        if (isNewImpl) {
+            when (action) {
+                UserActionModel.ENTER_GUEST_MODE ->
+                    guestUserInteractor.createAndSwitchTo(
+                        this::showDialog,
+                        this::dismissDialog,
+                        this::selectUser,
+                    )
+                UserActionModel.ADD_USER -> {
+                    val currentUser = repository.getSelectedUserInfo()
+                    showDialog(
+                        ShowDialogRequestModel.ShowAddUserDialog(
+                            userHandle = currentUser.userHandle,
+                            isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+                            showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+                        )
+                    )
+                }
+                UserActionModel.ADD_SUPERVISED_USER ->
+                    activityStarter.startActivity(
+                        Intent()
+                            .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
+                            .setPackage(supervisedUserPackageName)
+                            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+                        /* dismissShade= */ false,
+                    )
+                UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+                    activityStarter.startActivity(
+                        Intent(Settings.ACTION_USER_SETTINGS),
+                        /* dismissShade= */ false,
+                    )
+            }
+        } else {
+            when (action) {
+                UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
+                UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
+                UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
+                UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+                    activityStarter.startActivity(
+                        Intent(Settings.ACTION_USER_SETTINGS),
+                        /* dismissShade= */ false,
+                    )
+            }
         }
     }
+
+    fun exitGuestUser(
+        @UserIdInt guestUserId: Int,
+        @UserIdInt targetUserId: Int,
+        forceRemoveGuestOnExit: Boolean,
+    ) {
+        guestUserInteractor.exit(
+            guestUserId = guestUserId,
+            targetUserId = targetUserId,
+            forceRemoveGuestOnExit = forceRemoveGuestOnExit,
+            showDialog = this::showDialog,
+            dismissDialog = this::dismissDialog,
+            switchUser = this::switchUser,
+        )
+    }
+
+    fun removeGuestUser(
+        @UserIdInt guestUserId: Int,
+        @UserIdInt targetUserId: Int,
+    ) {
+        applicationScope.launch {
+            guestUserInteractor.remove(
+                guestUserId = guestUserId,
+                targetUserId = targetUserId,
+                ::showDialog,
+                ::dismissDialog,
+                ::selectUser,
+            )
+        }
+    }
+
+    private fun showDialog(request: ShowDialogRequestModel) {
+        _dialogShowRequests.value = request
+    }
+
+    private fun dismissDialog() {
+        _dialogDismissRequests.value = Unit
+    }
+
+    private fun notifyCallbacks() {
+        applicationScope.launch {
+            callbackMutex.withLock {
+                val iterator = callbacks.iterator()
+                while (iterator.hasNext()) {
+                    val callback = iterator.next()
+                    if (!callback.isEvictable()) {
+                        callback.onUserStateChanged()
+                    } else {
+                        iterator.remove()
+                    }
+                }
+            }
+        }
+    }
+
+    private suspend fun toRecord(
+        userInfo: UserInfo,
+        selectedUserId: Int,
+    ): UserRecord {
+        return LegacyUserDataHelper.createRecord(
+            context = applicationContext,
+            manager = manager,
+            userInfo = userInfo,
+            picture = null,
+            isCurrent = userInfo.id == selectedUserId,
+            canSwitchUsers = canSwitchUsers(selectedUserId),
+        )
+    }
+
+    private suspend fun toRecord(
+        action: UserActionModel,
+        selectedUserId: Int,
+        isAddFromLockscreenEnabled: Boolean,
+    ): UserRecord {
+        return LegacyUserDataHelper.createRecord(
+            context = applicationContext,
+            selectedUserId = selectedUserId,
+            actionType = action,
+            isRestricted =
+                if (action == UserActionModel.ENTER_GUEST_MODE) {
+                    // Entering guest mode is never restricted, so it's allowed to happen from the
+                    // lockscreen even if the "add from lockscreen" system setting is off.
+                    false
+                } else {
+                    !isAddFromLockscreenEnabled
+                },
+            isSwitchToEnabled =
+                canSwitchUsers(selectedUserId) &&
+                    // If the user is auto-created is must not be currently resetting.
+                    !(isGuestUserAutoCreated && isGuestUserResetting),
+        )
+    }
+
+    private fun switchUser(userId: Int) {
+        // TODO(b/246631653): track jank and lantecy like in the old impl.
+        refreshUsersScheduler.pause()
+        try {
+            activityManager.switchUser(userId)
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Couldn't switch user.", e)
+        }
+    }
+
+    private suspend fun onBroadcastReceived(
+        intent: Intent,
+        previousUserInfo: UserInfo?,
+    ) {
+        val shouldRefreshAllUsers =
+            when (intent.action) {
+                Intent.ACTION_USER_SWITCHED -> {
+                    dismissDialog()
+                    val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+                    if (previousUserInfo?.id != selectedUserId) {
+                        notifyCallbacks()
+                        restartSecondaryService(selectedUserId)
+                    }
+                    if (guestUserInteractor.isGuestUserAutoCreated) {
+                        guestUserInteractor.guaranteePresent()
+                    }
+                    true
+                }
+                Intent.ACTION_USER_INFO_CHANGED -> true
+                Intent.ACTION_USER_UNLOCKED -> {
+                    // If we unlocked the system user, we should refresh all users.
+                    intent.getIntExtra(
+                        Intent.EXTRA_USER_HANDLE,
+                        UserHandle.USER_NULL,
+                    ) == UserHandle.USER_SYSTEM
+                }
+                else -> true
+            }
+
+        if (shouldRefreshAllUsers) {
+            refreshUsersScheduler.unpauseAndRefresh()
+        }
+    }
+
+    private fun restartSecondaryService(@UserIdInt userId: Int) {
+        val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java)
+        // Disconnect from the old secondary user's service
+        val secondaryUserId = repository.secondaryUserId
+        if (secondaryUserId != UserHandle.USER_NULL) {
+            applicationContext.stopServiceAsUser(
+                intent,
+                UserHandle.of(secondaryUserId),
+            )
+            repository.secondaryUserId = UserHandle.USER_NULL
+        }
+
+        // Connect to the new secondary user's service (purely to ensure that a persistent
+        // SystemUI application is created for that user)
+        if (userId != UserHandle.USER_SYSTEM) {
+            applicationContext.startServiceAsUser(
+                intent,
+                UserHandle.of(userId),
+            )
+            repository.secondaryUserId = userId
+        }
+    }
+
+    private suspend fun toUserModels(
+        userInfos: List<UserInfo>,
+        selectedUserId: Int,
+        isUserSwitcherEnabled: Boolean,
+    ): List<UserModel> {
+        val canSwitchUsers = canSwitchUsers(selectedUserId)
+
+        return userInfos
+            // The guest user should go in the last position.
+            .sortedBy { it.isGuest }
+            .mapNotNull { userInfo ->
+                toUserModel(
+                    userInfo = userInfo,
+                    selectedUserId = selectedUserId,
+                    canSwitchUsers = canSwitchUsers,
+                    isUserSwitcherEnabled = isUserSwitcherEnabled,
+                )
+            }
+    }
+
+    private suspend fun toUserModel(
+        userInfo: UserInfo,
+        selectedUserId: Int,
+        canSwitchUsers: Boolean,
+        isUserSwitcherEnabled: Boolean,
+    ): UserModel? {
+        val userId = userInfo.id
+        val isSelected = userId == selectedUserId
+
+        return when {
+            // When the user switcher is not enabled in settings, we only show the primary user.
+            !isUserSwitcherEnabled && !userInfo.isPrimary -> null
+
+            // We avoid showing disabled users.
+            !userInfo.isEnabled -> null
+            userInfo.isGuest ->
+                UserModel(
+                    id = userId,
+                    name = Text.Loaded(userInfo.name),
+                    image =
+                        getUserImage(
+                            isGuest = true,
+                            userId = userId,
+                        ),
+                    isSelected = isSelected,
+                    isSelectable = canSwitchUsers,
+                    isGuest = true,
+                )
+            userInfo.supportsSwitchToByUser() ->
+                UserModel(
+                    id = userId,
+                    name = Text.Loaded(userInfo.name),
+                    image =
+                        getUserImage(
+                            isGuest = false,
+                            userId = userId,
+                        ),
+                    isSelected = isSelected,
+                    isSelectable = canSwitchUsers || isSelected,
+                    isGuest = false,
+                )
+            else -> null
+        }
+    }
+
+    private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
+        return withContext(backgroundDispatcher) {
+            manager.getUserSwitchability(UserHandle.of(selectedUserId))
+        } == UserManager.SWITCHABILITY_STATUS_OK
+    }
+
+    @SuppressLint("UseCompatLoadingForDrawables")
+    private suspend fun getUserImage(
+        isGuest: Boolean,
+        userId: Int,
+    ): Drawable {
+        if (isGuest) {
+            return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle))
+        }
+
+        // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them.
+        // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested.
+        val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) }
+        if (userIcon != null) {
+            return BitmapDrawable(userIcon)
+        }
+
+        return UserIcons.getDefaultUserIcon(
+            applicationContext.resources,
+            userId,
+            /* light= */ false
+        )
+    }
+
+    companion object {
+        private const val TAG = "UserInteractor"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
new file mode 100644
index 0000000..08d7c5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.user.domain.model
+
+import android.os.UserHandle
+
+/** Encapsulates a request to show a dialog. */
+sealed class ShowDialogRequestModel {
+    data class ShowAddUserDialog(
+        val userHandle: UserHandle,
+        val isKeyguardShowing: Boolean,
+        val showEphemeralMessage: Boolean,
+    ) : ShowDialogRequestModel()
+
+    data class ShowUserCreationDialog(
+        val isGuest: Boolean,
+    ) : ShowDialogRequestModel()
+
+    data class ShowExitGuestDialog(
+        val guestUserId: Int,
+        val targetUserId: Int,
+        val isGuestEphemeral: Boolean,
+        val isKeyguardShowing: Boolean,
+        val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
+    ) : ShowDialogRequestModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
new file mode 100644
index 0000000..137de15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.user.legacyhelper.data
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserManager
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.systemui.R
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+
+/**
+ * Defines utility functions for helping with legacy data code for users.
+ *
+ * We need these to avoid code duplication between logic inside the UserSwitcherController and in
+ * modern architecture classes such as repositories, interactors, and view-models. If we ever
+ * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites.
+ */
+object LegacyUserDataHelper {
+
+    @JvmStatic
+    fun createRecord(
+        context: Context,
+        manager: UserManager,
+        picture: Bitmap?,
+        userInfo: UserInfo,
+        isCurrent: Boolean,
+        canSwitchUsers: Boolean,
+    ): UserRecord {
+        val isGuest = userInfo.isGuest
+        return UserRecord(
+            info = userInfo,
+            picture =
+                getPicture(
+                    manager = manager,
+                    context = context,
+                    userInfo = userInfo,
+                    picture = picture,
+                ),
+            isGuest = isGuest,
+            isCurrent = isCurrent,
+            isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest),
+        )
+    }
+
+    @JvmStatic
+    fun createRecord(
+        context: Context,
+        selectedUserId: Int,
+        actionType: UserActionModel,
+        isRestricted: Boolean,
+        isSwitchToEnabled: Boolean,
+    ): UserRecord {
+        return UserRecord(
+            isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
+            isAddUser = actionType == UserActionModel.ADD_USER,
+            isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER,
+            isRestricted = isRestricted,
+            isSwitchToEnabled = isSwitchToEnabled,
+            enforcedAdmin =
+                getEnforcedAdmin(
+                    context = context,
+                    selectedUserId = selectedUserId,
+                ),
+        )
+    }
+
+    fun toUserActionModel(record: UserRecord): UserActionModel {
+        check(!isUser(record))
+
+        return when {
+            record.isAddUser -> UserActionModel.ADD_USER
+            record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
+            record.isGuest -> UserActionModel.ENTER_GUEST_MODE
+            else -> error("Not a known action: $record")
+        }
+    }
+
+    fun isUser(record: UserRecord): Boolean {
+        return record.info != null
+    }
+
+    private fun getEnforcedAdmin(
+        context: Context,
+        selectedUserId: Int,
+    ): EnforcedAdmin? {
+        val admin =
+            RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+                context,
+                UserManager.DISALLOW_ADD_USER,
+                selectedUserId,
+            )
+                ?: return null
+
+        return if (
+            !RestrictedLockUtilsInternal.hasBaseUserRestriction(
+                context,
+                UserManager.DISALLOW_ADD_USER,
+                selectedUserId,
+            )
+        ) {
+            admin
+        } else {
+            null
+        }
+    }
+
+    private fun getPicture(
+        context: Context,
+        manager: UserManager,
+        userInfo: UserInfo,
+        picture: Bitmap?,
+    ): Bitmap? {
+        if (userInfo.isGuest) {
+            return null
+        }
+
+        if (picture != null) {
+            return picture
+        }
+
+        val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null
+
+        val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size)
+        return Bitmap.createScaledBitmap(
+            unscaledOrNull,
+            avatarSize,
+            avatarSize,
+            /* filter= */ true,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
index bf7977a..2095683 100644
--- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
@@ -32,4 +32,6 @@
     val isSelected: Boolean,
     /** Whether this use is selectable. A non-selectable user cannot be switched to. */
     val isSelectable: Boolean,
+    /** Whether this model represents the guest user. */
+    val isGuest: Boolean,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
new file mode 100644
index 0000000..a9d66de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.user.ui.dialog
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.UserHandle
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.user.CreateUserActivity
+
+/** Dialog for adding a new user to the device. */
+class AddUserDialog(
+    context: Context,
+    userHandle: UserHandle,
+    isKeyguardShowing: Boolean,
+    showEphemeralMessage: Boolean,
+    private val falsingManager: FalsingManager,
+    private val broadcastSender: BroadcastSender,
+    private val dialogLaunchAnimator: DialogLaunchAnimator
+) : SystemUIDialog(context) {
+
+    private val onClickListener =
+        object : DialogInterface.OnClickListener {
+            override fun onClick(dialog: DialogInterface, which: Int) {
+                val penalty =
+                    if (which == BUTTON_NEGATIVE) {
+                        FalsingManager.NO_PENALTY
+                    } else {
+                        FalsingManager.MODERATE_PENALTY
+                    }
+                if (falsingManager.isFalseTap(penalty)) {
+                    return
+                }
+
+                if (which == BUTTON_NEUTRAL) {
+                    cancel()
+                    return
+                }
+
+                dialogLaunchAnimator.dismissStack(this@AddUserDialog)
+                if (ActivityManager.isUserAMonkey()) {
+                    return
+                }
+
+                // Use broadcast instead of ShadeController, as this dialog may have started in
+                // another
+                // process where normal dagger bindings are not available.
+                broadcastSender.sendBroadcastAsUser(
+                    Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+                    UserHandle.CURRENT
+                )
+
+                context.startActivityAsUser(
+                    CreateUserActivity.createIntentForStart(context),
+                    userHandle,
+                )
+            }
+        }
+
+    init {
+        setTitle(R.string.user_add_user_title)
+        val message =
+            context.getString(R.string.user_add_user_message_short) +
+                if (showEphemeralMessage) {
+                    context.getString(
+                        com.android.systemui.R.string.user_add_user_message_guest_remove
+                    )
+                } else {
+                    ""
+                }
+        setMessage(message)
+
+        setButton(
+            BUTTON_NEUTRAL,
+            context.getString(android.R.string.cancel),
+            onClickListener,
+        )
+
+        setButton(
+            BUTTON_POSITIVE,
+            context.getString(android.R.string.ok),
+            onClickListener,
+        )
+
+        setWindowOnTop(this, isKeyguardShowing)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
new file mode 100644
index 0000000..19ad44d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.user.ui.dialog
+
+import android.annotation.UserIdInt
+import android.content.Context
+import android.content.DialogInterface
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Dialog for exiting the guest user. */
+class ExitGuestDialog(
+    context: Context,
+    private val guestUserId: Int,
+    private val isGuestEphemeral: Boolean,
+    private val targetUserId: Int,
+    isKeyguardShowing: Boolean,
+    private val falsingManager: FalsingManager,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val onExitGuestUserListener: OnExitGuestUserListener,
+) : SystemUIDialog(context) {
+
+    fun interface OnExitGuestUserListener {
+        fun onExitGuestUser(
+            @UserIdInt guestId: Int,
+            @UserIdInt targetId: Int,
+            forceRemoveGuest: Boolean,
+        )
+    }
+
+    private val onClickListener =
+        object : DialogInterface.OnClickListener {
+            override fun onClick(dialog: DialogInterface, which: Int) {
+                val penalty =
+                    if (which == BUTTON_NEGATIVE) {
+                        FalsingManager.NO_PENALTY
+                    } else {
+                        FalsingManager.MODERATE_PENALTY
+                    }
+                if (falsingManager.isFalseTap(penalty)) {
+                    return
+                }
+
+                if (isGuestEphemeral) {
+                    if (which == BUTTON_POSITIVE) {
+                        dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+                        // Ephemeral guest: exit guest, guest is removed by the system
+                        // on exit, since its marked ephemeral
+                        onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false)
+                    } else if (which == BUTTON_NEGATIVE) {
+                        // Cancel clicked, do nothing
+                        cancel()
+                    }
+                } else {
+                    when (which) {
+                        BUTTON_POSITIVE -> {
+                            dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+                            // Non-ephemeral guest: exit guest, guest is not removed by the system
+                            // on exit, since its marked non-ephemeral
+                            onExitGuestUserListener.onExitGuestUser(
+                                guestUserId,
+                                targetUserId,
+                                false
+                            )
+                        }
+                        BUTTON_NEGATIVE -> {
+                            dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+                            // Non-ephemeral guest: remove guest and then exit
+                            onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true)
+                        }
+                        BUTTON_NEUTRAL -> {
+                            // Cancel clicked, do nothing
+                            cancel()
+                        }
+                    }
+                }
+            }
+        }
+
+    init {
+        if (isGuestEphemeral) {
+            setTitle(context.getString(R.string.guest_exit_dialog_title))
+            setMessage(context.getString(R.string.guest_exit_dialog_message))
+            setButton(
+                BUTTON_NEUTRAL,
+                context.getString(android.R.string.cancel),
+                onClickListener,
+            )
+            setButton(
+                BUTTON_POSITIVE,
+                context.getString(R.string.guest_exit_dialog_button),
+                onClickListener,
+            )
+        } else {
+            setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral))
+            setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral))
+            setButton(
+                BUTTON_NEUTRAL,
+                context.getString(android.R.string.cancel),
+                onClickListener,
+            )
+            setButton(
+                BUTTON_NEGATIVE,
+                context.getString(R.string.guest_exit_clear_data_button),
+                onClickListener,
+            )
+            setButton(
+                BUTTON_POSITIVE,
+                context.getString(R.string.guest_exit_save_data_button),
+                onClickListener,
+            )
+        }
+        setWindowOnTop(this, isKeyguardShowing)
+        setCanceledOnTouchOutside(false)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
new file mode 100644
index 0000000..c1d2f47
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.user.ui.dialog
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface UserDialogModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(UserSwitcherDialogCoordinator::class)
+    fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
new file mode 100644
index 0000000..6e7b523
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.user.ui.dialog
+
+import android.app.Dialog
+import android.content.Context
+import com.android.settingslib.users.UserCreatingDialog
+import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/** Coordinates dialogs for user switcher logic. */
+@SysUISingleton
+class UserSwitcherDialogCoordinator
+@Inject
+constructor(
+    @Application private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    private val falsingManager: FalsingManager,
+    private val broadcastSender: BroadcastSender,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val interactor: UserInteractor,
+    private val featureFlags: FeatureFlags,
+) : CoreStartable(context) {
+
+    private var currentDialog: Dialog? = null
+
+    override fun start() {
+        if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+            return
+        }
+
+        startHandlingDialogShowRequests()
+        startHandlingDialogDismissRequests()
+    }
+
+    private fun startHandlingDialogShowRequests() {
+        applicationScope.launch {
+            interactor.dialogShowRequests.filterNotNull().collect { request ->
+                currentDialog?.let {
+                    if (it.isShowing) {
+                        it.cancel()
+                    }
+                }
+
+                currentDialog =
+                    when (request) {
+                        is ShowDialogRequestModel.ShowAddUserDialog ->
+                            AddUserDialog(
+                                context = context,
+                                userHandle = request.userHandle,
+                                isKeyguardShowing = request.isKeyguardShowing,
+                                showEphemeralMessage = request.showEphemeralMessage,
+                                falsingManager = falsingManager,
+                                broadcastSender = broadcastSender,
+                                dialogLaunchAnimator = dialogLaunchAnimator,
+                            )
+                        is ShowDialogRequestModel.ShowUserCreationDialog ->
+                            UserCreatingDialog(
+                                context,
+                                request.isGuest,
+                            )
+                        is ShowDialogRequestModel.ShowExitGuestDialog ->
+                            ExitGuestDialog(
+                                context = context,
+                                guestUserId = request.guestUserId,
+                                isGuestEphemeral = request.isGuestEphemeral,
+                                targetUserId = request.targetUserId,
+                                isKeyguardShowing = request.isKeyguardShowing,
+                                falsingManager = falsingManager,
+                                dialogLaunchAnimator = dialogLaunchAnimator,
+                                onExitGuestUserListener = request.onExitGuestUser,
+                            )
+                    }
+
+                currentDialog?.show()
+                interactor.onDialogShown()
+            }
+        }
+    }
+
+    private fun startHandlingDialogDismissRequests() {
+        applicationScope.launch {
+            interactor.dialogDismissRequests.filterNotNull().collect {
+                currentDialog?.let {
+                    if (it.isShowing) {
+                        it.cancel()
+                    }
+                }
+
+                interactor.onDialogDismissed()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 398341d..5b83df7 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -21,7 +21,10 @@
 import androidx.lifecycle.ViewModelProvider
 import com.android.systemui.R
 import com.android.systemui.common.ui.drawable.CircularDrawable
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import com.android.systemui.user.shared.model.UserActionModel
@@ -36,9 +39,14 @@
 class UserSwitcherViewModel
 private constructor(
     private val userInteractor: UserInteractor,
+    private val guestUserInteractor: GuestUserInteractor,
     private val powerInteractor: PowerInteractor,
+    private val featureFlags: FeatureFlags,
 ) : ViewModel() {
 
+    private val isNewImpl: Boolean
+        get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
     /** On-device users. */
     val users: Flow<List<UserViewModel>> =
         userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -47,9 +55,6 @@
     val maximumUserColumns: Flow<Int> =
         users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
 
-    /** Whether the button to open the user action menu is visible. */
-    val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
-
     private val _isMenuVisible = MutableStateFlow(false)
     /**
      * Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
@@ -58,9 +63,23 @@
     val isMenuVisible: Flow<Boolean> = _isMenuVisible
     /** The user action menu. */
     val menu: Flow<List<UserActionViewModel>> =
-        userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
+        userInteractor.actions.map { actions ->
+            if (isNewImpl && actions.isNotEmpty()) {
+                    // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
+                    // switcher specific action that is not known to the our data source or other
+                    // features.
+                    actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+                } else {
+                    actions
+                }
+                .map { action -> toViewModel(action) }
+        }
+
+    /** Whether the button to open the user action menu is visible. */
+    val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
 
     private val hasCancelButtonBeenClicked = MutableStateFlow(false)
+    private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
 
     /**
      * Whether the observer should finish the experience. Once consumed, [onFinished] must be called
@@ -81,6 +100,7 @@
      */
     fun onFinished() {
         hasCancelButtonBeenClicked.value = false
+        isFinishRequiredDueToExecutedAction.value = false
     }
 
     /** Notifies that the user has clicked the "open menu" button. */
@@ -120,8 +140,10 @@
             },
             // When the cancel button is clicked, we should finish.
             hasCancelButtonBeenClicked,
-        ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
-            selectedUserChanged || screenTurnedOff || cancelButtonClicked
+            // If an executed action told us to finish, we should finish,
+            isFinishRequiredDueToExecutedAction,
+        ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
+            selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
         }
     }
 
@@ -164,13 +186,25 @@
                 } else {
                     LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
                         isGuest = model == UserActionModel.ENTER_GUEST_MODE,
-                        isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
-                        isGuestUserResetting = userInteractor.isGuestUserResetting,
+                        isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
+                        isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
                         isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
                         isAddUser = model == UserActionModel.ADD_USER,
                     )
                 },
-            onClicked = { userInteractor.executeAction(action = model) },
+            onClicked = {
+                userInteractor.executeAction(action = model)
+                // We don't finish because we want to show a dialog over the full-screen UI and
+                // that dialog can be dismissed in case the user changes their mind and decides not
+                // to add a user.
+                //
+                // We finish for all other actions because they navigate us away from the
+                // full-screen experience or are destructive (like changing to the guest user).
+                val shouldFinish = model != UserActionModel.ADD_USER
+                if (shouldFinish) {
+                    isFinishRequiredDueToExecutedAction.value = true
+                }
+            },
         )
     }
 
@@ -186,13 +220,17 @@
     @Inject
     constructor(
         private val userInteractor: UserInteractor,
+        private val guestUserInteractor: GuestUserInteractor,
         private val powerInteractor: PowerInteractor,
+        private val featureFlags: FeatureFlags,
     ) : ViewModelProvider.Factory {
         override fun <T : ViewModel> create(modelClass: Class<T>): T {
             @Suppress("UNCHECKED_CAST")
             return UserSwitcherViewModel(
                 userInteractor = userInteractor,
+                guestUserInteractor = guestUserInteractor,
                 powerInteractor = powerInteractor,
+                featureFlags = featureFlags,
             )
                 as T
         }
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
new file mode 100644
index 0000000..0b8257d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.util.settings
+
+import android.annotation.UserIdInt
+import android.database.ContentObserver
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Kotlin extension functions for [SettingsProxy]. */
+object SettingsProxyExt {
+
+    /** Returns a flow of [Unit] that is invoked each time that content is updated. */
+    fun SettingsProxy.observerFlow(
+        vararg names: String,
+        @UserIdInt userId: Int = UserHandle.USER_CURRENT,
+    ): Flow<Unit> {
+        return conflatedCallbackFlow {
+            val observer =
+                object : ContentObserver(null) {
+                    override fun onChange(selfChange: Boolean) {
+                        trySend(Unit)
+                    }
+                }
+
+            names.forEach { name -> registerContentObserverForUser(name, observer, userId) }
+
+            awaitClose { unregisterContentObserver(observer) }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 43f6f1a..c1036e3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -411,7 +411,7 @@
                     0 /* flags */);
             users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
                     false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
-                    false /* isAddSupervisedUser */));
+                    false /* isAddSupervisedUser */, null /* enforcedAdmin */));
         }
         return users;
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 12d3d42..14b637c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -60,6 +60,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
@@ -80,13 +81,13 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.service.dreams.IDreamManager;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableContext;
 import android.testing.TestableLooper;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
@@ -171,6 +172,8 @@
     @Mock
     private DevicePolicyManager mDevicePolicyManager;
     @Mock
+    private IDreamManager mDreamManager;
+    @Mock
     private KeyguardBypassController mKeyguardBypassController;
     @Mock
     private SubscriptionManager mSubscriptionManager;
@@ -179,6 +182,8 @@
     @Mock
     private TelephonyManager mTelephonyManager;
     @Mock
+    private SensorPrivacyManager mSensorPrivacyManager;
+    @Mock
     private StatusBarStateController mStatusBarStateController;
     @Mock
     private AuthController mAuthController;
@@ -220,7 +225,6 @@
     private TestableLooper mTestableLooper;
     private Handler mHandler;
     private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private TestableContext mSpiedContext;
     private MockitoSession mMockitoSession;
     private StatusBarStateController.StateListener mStatusBarStateListener;
     private IBiometricEnabledOnKeyguardCallback mBiometricEnabledOnKeyguardCallback;
@@ -229,9 +233,6 @@
     @Before
     public void setup() throws RemoteException {
         MockitoAnnotations.initMocks(this);
-        mSpiedContext = spy(mContext);
-        when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true);
-        when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
         when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo);
         when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId);
         when(mFaceManager.isHardwareDetected()).thenReturn(true);
@@ -280,14 +281,6 @@
                 .thenReturn(new ServiceState());
         when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings);
         when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
-        mSpiedContext.addMockSystemService(TrustManager.class, mTrustManager);
-        mSpiedContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
-        mSpiedContext.addMockSystemService(BiometricManager.class, mBiometricManager);
-        mSpiedContext.addMockSystemService(FaceManager.class, mFaceManager);
-        mSpiedContext.addMockSystemService(UserManager.class, mUserManager);
-        mSpiedContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
-        mSpiedContext.addMockSystemService(SubscriptionManager.class, mSubscriptionManager);
-        mSpiedContext.addMockSystemService(TelephonyManager.class, mTelephonyManager);
 
         mMockitoSession = ExtendedMockito.mockitoSession()
                 .spyStatic(SubscriptionManager.class)
@@ -302,7 +295,7 @@
 
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
 
         verify(mBiometricManager)
                 .registerEnabledOnKeyguardCallback(mBiometricEnabledCallbackArgCaptor.capture());
@@ -357,7 +350,7 @@
         when(mTelephonyManager.getSimState(anyInt())).thenReturn(state);
         when(mSubscriptionManager.getSubscriptionIds(anyInt())).thenReturn(new int[]{subId});
 
-        KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mSpiedContext);
+        KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mContext);
 
         mTestableLooper.processAllMessages();
 
@@ -1203,9 +1196,9 @@
     @Test
     public void testShouldListenForFace_whenFaceManagerNotAvailable_returnsFalse() {
         cleanupKeyguardUpdateMonitor();
-        mSpiedContext.addMockSystemService(FaceManager.class, null);
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false);
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+        mFaceManager = null;
+
+        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
     }
@@ -1259,7 +1252,7 @@
         // This disables face auth
         when(mUserManager.isPrimaryUser()).thenReturn(false);
         mKeyguardUpdateMonitor =
-                new TestableKeyguardUpdateMonitor(mSpiedContext);
+                new TestableKeyguardUpdateMonitor(mContext);
 
         // Face auth should run when the following is true.
         keyguardNotGoingAway();
@@ -1528,15 +1521,16 @@
         verify(mHandler, times(1)).removeCallbacks(mKeyguardUpdateMonitor.mFpCancelNotReceived);
         mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
         mTestableLooper.processAllMessages();
-        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(anyBoolean())).isEqualTo(true);
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isEqualTo(true);
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(true);
     }
 
     @Test
     public void testFingerAcquired_wakesUpPowerManager() {
         cleanupKeyguardUpdateMonitor();
-        mSpiedContext.getOrCreateTestableResources().addOverride(
+        mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.kg_wake_on_acquire_start, true);
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
         fingerprintAcquireStart();
 
         verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
@@ -1545,9 +1539,9 @@
     @Test
     public void testFingerAcquired_doesNotWakeUpPowerManager() {
         cleanupKeyguardUpdateMonitor();
-        mSpiedContext.getOrCreateTestableResources().addOverride(
+        mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.kg_wake_on_acquire_start, false);
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
         fingerprintAcquireStart();
 
         verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
@@ -1717,7 +1711,9 @@
                     mAuthController, mTelephonyListenerManager,
                     mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
                     mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
-                    mPowerManager);
+                    mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
+                    mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
+                    mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 6436981..781dc15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -144,6 +144,12 @@
         mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
         clearInvocations(mMachine);
 
+        ArgumentCaptor<Boolean> boolCaptor = ArgumentCaptor.forClass(Boolean.class);
+        doAnswer(invocation ->
+                when(mHost.isPulsePending()).thenReturn(boolCaptor.getValue())
+        ).when(mHost).setPulsePending(boolCaptor.capture());
+
+        when(mHost.isPulsingBlocked()).thenReturn(false);
         mProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 1));
         captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
         mProximitySensor.alertListeners();
@@ -160,6 +166,29 @@
     }
 
     @Test
+    public void testOnNotification_noPulseIfPulseIsNotPendingAnymore() {
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+        ArgumentCaptor<DozeHost.Callback> captor = ArgumentCaptor.forClass(DozeHost.Callback.class);
+        doAnswer(invocation -> null).when(mHost).addCallback(captor.capture());
+
+        mTriggers.transitionTo(UNINITIALIZED, DozeMachine.State.INITIALIZED);
+        mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
+        clearInvocations(mMachine);
+        when(mHost.isPulsingBlocked()).thenReturn(false);
+
+        // GIVEN pulsePending = false
+        when(mHost.isPulsePending()).thenReturn(false);
+
+        // WHEN prox check returns FAR
+        mProximitySensor.setLastEvent(new ThresholdSensorEvent(false, 2));
+        captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
+        mProximitySensor.alertListeners();
+
+        // THEN don't request pulse because the pending pulse was abandoned early
+        verify(mMachine, never()).requestPulse(anyInt());
+    }
+
+    @Test
     public void testTransitionTo_disablesAndEnablesTouchSensors() {
         when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
 
@@ -237,6 +266,11 @@
         when(mSessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD))
                 .thenReturn(keyguardSessionId);
 
+        ArgumentCaptor<Boolean> boolCaptor = ArgumentCaptor.forClass(Boolean.class);
+        doAnswer(invocation ->
+                when(mHost.isPulsePending()).thenReturn(boolCaptor.getValue())
+        ).when(mHost).setPulsePending(boolCaptor.capture());
+
         // WHEN quick pick up is triggered
         mTriggers.onSensor(DozeLog.REASON_SENSOR_QUICK_PICKUP, 100, 100, null);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index ba1e168..eea2e95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -116,6 +116,7 @@
         val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
 
         assertThat(latest).isFalse()
+        assertThat(underTest.isKeyguardShowing()).isFalse()
 
         val captor = argumentCaptor<KeyguardStateController.Callback>()
         verify(keyguardStateController).addCallback(captor.capture())
@@ -123,10 +124,12 @@
         whenever(keyguardStateController.isShowing).thenReturn(true)
         captor.value.onKeyguardShowingChanged()
         assertThat(latest).isTrue()
+        assertThat(underTest.isKeyguardShowing()).isTrue()
 
         whenever(keyguardStateController.isShowing).thenReturn(false)
         captor.value.onKeyguardShowingChanged()
         assertThat(latest).isFalse()
+        assertThat(underTest.isKeyguardShowing()).isFalse()
 
         job.cancel()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
index 82aa612..5973340 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
@@ -26,13 +26,13 @@
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.arch.core.executor.TaskExecutor
 import androidx.test.filters.SmallTest
-
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.Classifier
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.concurrency.FakeRepeatableExecutor
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
-
 import org.junit.After
 import org.junit.Before
 import org.junit.Ignore
@@ -47,8 +47,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -70,6 +70,8 @@
     }
     @Mock private lateinit var mockController: MediaController
     @Mock private lateinit var mockTransport: MediaController.TransportControls
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var mockBar: SeekBar
     private val token1 = MediaSession.Token(1, null)
     private val token2 = MediaSession.Token(2, null)
 
@@ -78,9 +80,10 @@
     @Before
     fun setUp() {
         fakeExecutor = FakeExecutor(FakeSystemClock())
-        viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor))
+        viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
         viewModel.logSeek = { }
         whenever(mockController.sessionToken).thenReturn(token1)
+        whenever(mockBar.context).thenReturn(context)
 
         // LiveData to run synchronously
         ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
@@ -454,6 +457,25 @@
     }
 
     @Test
+    fun onFalseTapOrTouch() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true)
+        whenever(falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
+        viewModel.updateController(mockController)
+        val pos = 169
+
+        viewModel.attachTouchHandlers(mockBar)
+        with(viewModel.seekBarListener) {
+            onStartTrackingTouch(mockBar)
+            onProgressChanged(mockBar, pos, true)
+            onStopTrackingTouch(mockBar)
+        }
+
+        // THEN transport controls should not be used
+        verify(mockTransport, never()).seekTo(pos.toLong())
+    }
+
+    @Test
     fun queuePollTaskWhenPlaying() {
         // GIVEN that the track is playing
         val state = PlaybackState.Builder().run {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index ff0faf9..098086a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -35,7 +35,9 @@
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -43,10 +45,12 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
@@ -75,6 +79,14 @@
     private lateinit var windowManager: WindowManager
     @Mock
     private lateinit var commandQueue: CommandQueue
+    @Mock
+    private lateinit var lazyFalsingManager: Lazy<FalsingManager>
+    @Mock
+    private lateinit var falsingManager: FalsingManager
+    @Mock
+    private lateinit var lazyFalsingCollector: Lazy<FalsingCollector>
+    @Mock
+    private lateinit var falsingCollector: FalsingCollector
     private lateinit var commandQueueCallback: CommandQueue.Callbacks
     private lateinit var fakeAppIconDrawable: Drawable
     private lateinit var fakeClock: FakeSystemClock
@@ -101,6 +113,8 @@
         senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
+        whenever(lazyFalsingManager.get()).thenReturn(falsingManager)
+        whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector)
 
         controllerSender = MediaTttChipControllerSender(
             commandQueue,
@@ -111,7 +125,9 @@
             accessibilityManager,
             configurationController,
             powerManager,
-            senderUiEventLogger
+            senderUiEventLogger,
+            lazyFalsingManager,
+            lazyFalsingCollector
         )
 
         val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
@@ -417,6 +433,38 @@
     }
 
     @Test
+    fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
+        whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(true)
+        var undoCallbackCalled = false
+        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+            override fun onUndoTriggered() {
+                undoCallbackCalled = true
+            }
+        }
+
+        controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+        getChipView().getUndoButton().performClick()
+
+        assertThat(undoCallbackCalled).isFalse()
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
+        whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(false)
+        var undoCallbackCalled = false
+        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+            override fun onUndoTriggered() {
+                undoCallbackCalled = true
+            }
+        }
+
+        controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+        getChipView().getUndoButton().performClick()
+
+        assertThat(undoCallbackCalled).isTrue()
+    }
+
+    @Test
     fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
         val undoCallback = object : IUndoMediaTransferCallback.Stub() {
             override fun onUndoTriggered() {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 2a66773..d2c2d58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -14,6 +14,9 @@
 
 package com.android.systemui.qs;
 
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertTrue;
@@ -49,13 +52,13 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.media.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
@@ -93,7 +96,7 @@
     @Mock private QSPanel.QSTileLayout mQsTileLayout;
     @Mock private QSPanel.QSTileLayout mQQsTileLayout;
     @Mock private QSAnimator mQSAnimator;
-    @Mock private StatusBarStateController mStatusBarStateController;
+    @Mock private SysuiStatusBarStateController mStatusBarStateController;
     @Mock private QSSquishinessController mSquishinessController;
     private View mQsFragmentView;
 
@@ -158,7 +161,7 @@
     public void
             transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() {
         QSFragment fragment = resumeAndGetFragment();
-        setStatusBarState(StatusBarState.KEYGUARD);
+        setStatusBarState(KEYGUARD);
         when(mQSPanelController.isBouncerInTransit()).thenReturn(false);
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.5f;
@@ -174,7 +177,7 @@
     public void
             transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() {
         QSFragment fragment = resumeAndGetFragment();
-        setStatusBarState(StatusBarState.KEYGUARD);
+        setStatusBarState(KEYGUARD);
         when(mQSPanelController.isBouncerInTransit()).thenReturn(true);
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.5f;
@@ -262,6 +265,27 @@
     }
 
     @Test
+    public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() {
+        QSFragment fragment = resumeAndGetFragment();
+        enableSplitShade();
+        when(mStatusBarStateController.getState()).thenReturn(SHADE);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+        boolean isTransitioningToFullShade = false;
+        float transitionProgress = 0;
+        float squishinessFraction = 0f;
+
+        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+                squishinessFraction);
+
+        // trigger alpha refresh with non-zero expansion and fraction values
+        fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1,
+                /* proposedTranslation= */ 0, /* squishinessFraction= */ 1);
+
+        // alpha should follow lockscreen to shade progress, not panel expansion fraction
+        assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress);
+    }
+
+    @Test
     public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() {
         QSFragment fragment = resumeAndGetFragment();
         disableSplitShade();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
index 7d56339..4c44dac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
@@ -33,6 +33,7 @@
 import android.graphics.Paint;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.provider.MediaStore;
 import android.testing.AndroidTestingRunner;
 
@@ -97,7 +98,8 @@
         Bitmap original = createCheckerBitmap(10, 10, 10);
 
         ListenableFuture<ImageExporter.Result> direct =
-                exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME);
+                exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME,
+                        Process.myUserHandle());
         assertTrue("future should be done", direct.isDone());
         assertFalse("future should not be canceled", direct.isCancelled());
         ImageExporter.Result result = direct.get();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
index 69b7b88..8c9404e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -180,7 +180,7 @@
         data.finisher = null;
         data.mActionsReadyListener = null;
         SaveImageInBackgroundTask task =
-                new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+                new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
                         ActionTransition::new, mSmartActionsProvider);
 
         Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
@@ -208,7 +208,7 @@
         data.finisher = null;
         data.mActionsReadyListener = null;
         SaveImageInBackgroundTask task =
-                new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+                new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
                         ActionTransition::new, mSmartActionsProvider);
 
         Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
@@ -236,7 +236,7 @@
         data.finisher = null;
         data.mActionsReadyListener = null;
         SaveImageInBackgroundTask task =
-                new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+                new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
                         ActionTransition::new, mSmartActionsProvider);
 
         Notification.Action deleteAction = task.createDeleteAction(mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
deleted file mode 100644
index eb4cca8..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2017 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;
-
-import static org.junit.Assert.assertFalse;
-
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Verifies that particular sets of dependencies don't have dependencies on others. For example,
- * code managing notifications shouldn't directly depend on CentralSurfaces, since there are
- * platforms which want to manage notifications, but don't use CentralSurfaces.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class NonPhoneDependencyTest extends SysuiTestCase {
-    @Mock private NotificationPresenter mPresenter;
-    @Mock private NotificationListContainer mListContainer;
-    @Mock private RemoteInputController.Delegate mDelegate;
-    @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback;
-    @Mock private OnSettingsClickListener mOnSettingsClickListener;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
-        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-               new Handler(TestableLooper.get(this).getLooper()));
-    }
-
-    @Ignore("Causes binder calls which fail")
-    @Test
-    public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() {
-        NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class);
-        NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class);
-        NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class);
-        NotificationRemoteInputManager remoteInputManager =
-                Dependency.get(NotificationRemoteInputManager.class);
-        NotificationLockscreenUserManager lockscreenUserManager =
-                Dependency.get(NotificationLockscreenUserManager.class);
-        gutsManager.setUpWithPresenter(mPresenter, mListContainer,
-                mOnSettingsClickListener);
-        notificationLogger.setUpWithContainer(mListContainer);
-        mediaManager.setUpWithPresenter(mPresenter);
-        remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
-                mDelegate);
-        lockscreenUserManager.setUpWithPresenter(mPresenter);
-
-        TestableLooper.get(this).processAllMessages();
-        assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 853d1df..bdafa48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -88,6 +89,8 @@
     @Mock
     private NotificationClickNotifier mClickNotifier;
     @Mock
+    private OverviewProxyService mOverviewProxyService;
+    @Mock
     private KeyguardManager mKeyguardManager;
     @Mock
     private DeviceProvisionedController mDeviceProvisionedController;
@@ -344,6 +347,7 @@
                     (() -> mVisibilityProvider),
                     (() -> mNotifCollection),
                     mClickNotifier,
+                    (() -> mOverviewProxyService),
                     NotificationLockscreenUserManagerTest.this.mKeyguardManager,
                     mStatusBarStateController,
                     Handler.createAsync(Looper.myLooper()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2ee3126..2970807 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -337,6 +337,40 @@
     }
 
     @Test
+    fun testOnEntryUpdated_toAlert() {
+        // GIVEN that an entry is posted that should not heads up
+        setShouldHeadsUp(mEntry, false)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // WHEN it's updated to heads up
+        setShouldHeadsUp(mEntry)
+        mCollectionListener.onEntryUpdated(mEntry)
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification alerts
+        finishBind(mEntry)
+        verify(mHeadsUpManager).showNotification(mEntry)
+    }
+
+    @Test
+    fun testOnEntryUpdated_toNotAlert() {
+        // GIVEN that an entry is posted that should heads up
+        setShouldHeadsUp(mEntry)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // WHEN it's updated to not heads up
+        setShouldHeadsUp(mEntry, false)
+        mCollectionListener.onEntryUpdated(mEntry)
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is never bound or shown
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        verify(mHeadsUpManager, never()).showNotification(any())
+    }
+
+    @Test
     fun testOnEntryRemovedRemovesHeadsUpNotification() {
         // GIVEN the current HUN is mEntry
         addHUN(mEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
new file mode 100644
index 0000000..f4d61c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
@@ -0,0 +1,323 @@
+/*
+ *
+ * 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.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryMonitorTest : SysuiTestCase() {
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification() {
+        val notification = createBasicNotification().build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+        val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = 0,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
+        val notification =
+            createBasicNotification()
+                .setCustomContentView(
+                    RemoteViews(context.packageName, android.R.layout.list_content)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3384,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = true,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
+        val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
+        val notification =
+            createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = 444444,
+            largeIcon = 0,
+            extras = 3212,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_bigPictureStyle() {
+        val bigPicture =
+            Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
+        val bigPictureIcon =
+            Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.BigPictureStyle()
+                        .bigPicture(bigPicture)
+                        .bigLargeIcon(bigPictureIcon)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4092,
+            bigPicture = 960000,
+            extender = 0,
+            style = "BigPictureStyle",
+            styleIcon = bigPictureIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_callingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val fakeIntent =
+            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+        val notification =
+            createBasicNotification()
+                .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4084,
+            bigPicture = 0,
+            extender = 0,
+            style = "CallStyle",
+            styleIcon = personIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_messagingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val message = Notification.MessagingStyle.Message("Message!", 4323, person)
+        val historicPersonIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
+        val historicPerson =
+            Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
+        val historicMessage =
+            Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
+
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.MessagingStyle(person)
+                        .addMessage(message)
+                        .addHistoricMessage(historicMessage)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 5024,
+            bigPicture = 0,
+            extender = 0,
+            style = "MessagingStyle",
+            styleIcon =
+                personIcon.bitmap.allocationByteCount +
+                    historicPersonIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_carExtender() {
+        val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
+        val extender = Notification.CarExtender().setLargeIcon(carIcon)
+        val notification = createBasicNotification().extend(extender).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3612,
+            bigPicture = 0,
+            extender = 556656,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_tvWearExtender() {
+        val tvExtender = Notification.TvExtender().setChannel("channel2")
+        val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
+        val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
+        val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3820,
+            bigPicture = 0,
+            extender = 388 + wearBackground.allocationByteCount,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    private fun createBasicNotification(): Notification.Builder {
+        val smallIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
+        val largeIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
+        return Notification.Builder(context)
+            .setSmallIcon(smallIcon)
+            .setLargeIcon(largeIcon)
+            .setContentTitle("This is a title")
+            .setContentText("This is content text.")
+    }
+
+    /** This will generate a nicer error message than comparing objects */
+    private fun assertNotificationObjectSizes(
+        memoryUse: NotificationMemoryUsage,
+        smallIcon: Int,
+        largeIcon: Int,
+        extras: Int,
+        bigPicture: Int,
+        extender: Int,
+        style: String?,
+        styleIcon: Int,
+        hasCustomView: Boolean
+    ) {
+        assertThat(memoryUse.packageName).isEqualTo("test_pkg")
+        assertThat(memoryUse.notificationId)
+            .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
+        assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
+        assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
+        assertThat(memoryUse.objectUsage.extras).isEqualTo(extras)
+        assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
+        assertThat(memoryUse.objectUsage.extender).isEqualTo(extender)
+        if (style == null) {
+            assertThat(memoryUse.objectUsage.style).isNull()
+        } else {
+            assertThat(memoryUse.objectUsage.style).isEqualTo(style)
+        }
+        assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
+        assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
+    }
+
+    private fun getUseObject(
+        singleItemUseList: List<NotificationMemoryUsage>
+    ): NotificationMemoryUsage {
+        assertThat(singleItemUseList).hasSize(1)
+        return singleItemUseList[0]
+    }
+
+    private fun createNMMWithNotifications(
+        notifications: List<Notification>
+    ): NotificationMemoryMonitor {
+        val notifPipeline: NotifPipeline = mock()
+        val notificationEntries =
+            notifications.map { n ->
+                NotificationEntryBuilder().setTag("test").setNotification(n).build()
+            }
+        whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
+        return NotificationMemoryMonitor(notifPipeline, mock())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
index 7e97629..dae0aa2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
@@ -40,6 +40,10 @@
                 NotificationPanelLogger.toNotificationProto(visibleNotifications)));
     }
 
+    @Override
+    public void logNotificationDrag(NotificationEntry draggedNotification) {
+    }
+
     public static class CallRecord {
         public boolean isLockscreen;
         public Notifications.NotificationList list;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 922e93d..ed2afe7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -40,6 +40,8 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import org.junit.Before;
@@ -63,6 +65,7 @@
     private NotificationMenuRowPlugin.MenuItem mMenuItem =
             mock(NotificationMenuRowPlugin.MenuItem.class);
     private ShadeController mShadeController = mock(ShadeController.class);
+    private NotificationPanelLogger mNotificationPanelLogger = mock(NotificationPanelLogger.class);
 
     @Before
     public void setUp() throws Exception {
@@ -82,7 +85,7 @@
         when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
 
         mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager,
-                mShadeController);
+                mShadeController, mNotificationPanelLogger);
     }
 
     @Test
@@ -96,6 +99,7 @@
         mRow.doDragCallback(0, 0);
         verify(controller).startDragAndDrop(mRow);
         verify(mHeadsUpManager, times(1)).releaseAllImmediately();
+        verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
     }
 
     @Test
@@ -107,6 +111,7 @@
         verify(controller).startDragAndDrop(mRow);
         verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
                 eq(false), anyFloat());
+        verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
     }
 
     @Test
@@ -124,6 +129,7 @@
 
         // Verify that we never start the actual drag since there is no content
         verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt());
+        verify(mNotificationPanelLogger, never()).logNotificationDrag(any());
     }
 
     private ExpandableNotificationRowDragController createSpyController() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 9de9db1..996851e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -40,7 +40,6 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
-import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -73,7 +72,6 @@
     @Mock private HeadsUpManagerPhone mHeadsUpManager;
     @Mock private ScrimController mScrimController;
     @Mock private DozeScrimController mDozeScrimController;
-    @Mock private KeyguardViewMediator mKeyguardViewMediator;
     @Mock private StatusBarStateControllerImpl mStatusBarStateController;
     @Mock private BatteryController mBatteryController;
     @Mock private DeviceProvisionedController mDeviceProvisionedController;
@@ -101,7 +99,7 @@
         mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
                 mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
                 mBatteryController, mScrimController, () -> mBiometricUnlockController,
-                mKeyguardViewMediator, () -> mAssistManager, mDozeScrimController,
+                () -> mAssistManager, mDozeScrimController,
                 mKeyguardUpdateMonitor, mPulseExpansionHandler,
                 mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
                 mAuthController, mNotificationIconAreaController);
@@ -132,19 +130,11 @@
         verify(mStatusBarStateController).setIsDozing(eq(false));
     }
 
-
     @Test
     public void testPulseWhileDozing_updatesScrimController() {
         mCentralSurfaces.setBarStateForTest(StatusBarState.KEYGUARD);
         mCentralSurfaces.showKeyguardImpl();
 
-        // Keep track of callback to be able to stop the pulse
-//        DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
-//        doAnswer(invocation -> {
-//            pulseCallback[0] = invocation.getArgument(0);
-//            return null;
-//        }).when(mDozeScrimController).pulse(any(), anyInt());
-
         // Starting a pulse should change the scrim controller to the pulsing state
         mDozeServiceHost.pulseWhileDozing(new DozeHost.PulseCallback() {
             @Override
@@ -210,4 +200,17 @@
             }
         }
     }
+
+    @Test
+    public void testStopPulsing_setPendingPulseToFalse() {
+        // GIVEN a pending pulse
+        mDozeServiceHost.setPulsePending(true);
+
+        // WHEN pulsing is stopped
+        mDozeServiceHost.stopPulsing();
+
+        // THEN isPendingPulse=false, pulseOutNow is called
+        assertFalse(mDozeServiceHost.isPulsePending());
+        verify(mDozeScrimController).pulseOutNow();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index cfaa470..6ec5cf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -47,6 +47,7 @@
 import com.android.keyguard.CarrierTextController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.battery.BatteryMeterViewController;
@@ -123,6 +124,7 @@
     private StatusBarUserInfoTracker mStatusBarUserInfoTracker;
     @Mock private SecureSettings mSecureSettings;
     @Mock private CommandQueue mCommandQueue;
+    @Mock private KeyguardLogger mLogger;
 
     private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider;
     private KeyguardStatusBarView mKeyguardStatusBarView;
@@ -172,7 +174,8 @@
                 mStatusBarUserInfoTracker,
                 mSecureSettings,
                 mCommandQueue,
-                mFakeExecutor
+                mFakeExecutor,
+                mLogger
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
new file mode 100644
index 0000000..773a0d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.telephony.data.repository
+
+import android.telephony.TelephonyCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.telephony.TelephonyListenerManager
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class TelephonyRepositoryImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var manager: TelephonyListenerManager
+
+    private lateinit var underTest: TelephonyRepositoryImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            TelephonyRepositoryImpl(
+                manager = manager,
+            )
+    }
+
+    @Test
+    fun callState() =
+        runBlocking(IMMEDIATE) {
+            var callState: Int? = null
+            val job = underTest.callState.onEach { callState = it }.launchIn(this)
+            val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
+            verify(manager).addCallStateListener(listenerCaptor.capture())
+            val listener = listenerCaptor.value
+
+            listener.onCallStateChanged(0)
+            assertThat(callState).isEqualTo(0)
+
+            listener.onCallStateChanged(1)
+            assertThat(callState).isEqualTo(1)
+
+            listener.onCallStateChanged(2)
+            assertThat(callState).isEqualTo(2)
+
+            job.cancel()
+
+            verify(manager).removeCallStateListener(listener)
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
new file mode 100644
index 0000000..4a8e055
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.user.data.repository
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {
+
+    @Before
+    fun setUp() {
+        super.setUp(isRefactored = true)
+    }
+
+    @Test
+    fun userSwitcherSettings() = runSelfCancelingTest {
+        setUpGlobalSettings(
+            isSimpleUserSwitcher = true,
+            isAddUsersFromLockscreen = true,
+            isUserSwitcherEnabled = true,
+        )
+        underTest = create(this)
+
+        var value: UserSwitcherSettingsModel? = null
+        underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
+
+        assertUserSwitcherSettings(
+            model = value,
+            expectedSimpleUserSwitcher = true,
+            expectedAddUsersFromLockscreen = true,
+            expectedUserSwitcherEnabled = true,
+        )
+
+        setUpGlobalSettings(
+            isSimpleUserSwitcher = false,
+            isAddUsersFromLockscreen = true,
+            isUserSwitcherEnabled = true,
+        )
+        assertUserSwitcherSettings(
+            model = value,
+            expectedSimpleUserSwitcher = false,
+            expectedAddUsersFromLockscreen = true,
+            expectedUserSwitcherEnabled = true,
+        )
+    }
+
+    @Test
+    fun refreshUsers() = runSelfCancelingTest {
+        underTest = create(this)
+        val initialExpectedValue =
+            setUpUsers(
+                count = 3,
+                selectedIndex = 0,
+            )
+        var userInfos: List<UserInfo>? = null
+        var selectedUserInfo: UserInfo? = null
+        underTest.userInfos.onEach { userInfos = it }.launchIn(this)
+        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
+
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(initialExpectedValue)
+        assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
+        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+        val secondExpectedValue =
+            setUpUsers(
+                count = 4,
+                selectedIndex = 1,
+            )
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(secondExpectedValue)
+        assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
+        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+        val selectedNonGuestUserId = selectedUserInfo?.id
+        val thirdExpectedValue =
+            setUpUsers(
+                count = 2,
+                hasGuest = true,
+                selectedIndex = 1,
+            )
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(thirdExpectedValue)
+        assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
+        assertThat(selectedUserInfo?.isGuest).isTrue()
+        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+    }
+
+    private fun setUpUsers(
+        count: Int,
+        hasGuest: Boolean = false,
+        selectedIndex: Int = 0,
+    ): List<UserInfo> {
+        val userInfos =
+            (0 until count).map { index ->
+                createUserInfo(
+                    index,
+                    isGuest = hasGuest && index == count - 1,
+                )
+            }
+        whenever(manager.aliveUsers).thenReturn(userInfos)
+        tracker.set(userInfos, selectedIndex)
+        return userInfos
+    }
+
+    private fun createUserInfo(
+        id: Int,
+        isGuest: Boolean,
+    ): UserInfo {
+        val flags = 0
+        return UserInfo(
+            id,
+            "user_$id",
+            /* iconPath= */ "",
+            flags,
+            if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
+        )
+    }
+
+    private fun setUpGlobalSettings(
+        isSimpleUserSwitcher: Boolean = false,
+        isAddUsersFromLockscreen: Boolean = false,
+        isUserSwitcherEnabled: Boolean = true,
+    ) {
+        context.orCreateTestableResources.addOverride(
+            com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
+            true,
+        )
+        globalSettings.putIntForUser(
+            UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
+            if (isSimpleUserSwitcher) 1 else 0,
+            UserHandle.USER_SYSTEM,
+        )
+        globalSettings.putIntForUser(
+            Settings.Global.ADD_USERS_WHEN_LOCKED,
+            if (isAddUsersFromLockscreen) 1 else 0,
+            UserHandle.USER_SYSTEM,
+        )
+        globalSettings.putIntForUser(
+            Settings.Global.USER_SWITCHER_ENABLED,
+            if (isUserSwitcherEnabled) 1 else 0,
+            UserHandle.USER_SYSTEM,
+        )
+    }
+
+    private fun assertUserSwitcherSettings(
+        model: UserSwitcherSettingsModel?,
+        expectedSimpleUserSwitcher: Boolean,
+        expectedAddUsersFromLockscreen: Boolean,
+        expectedUserSwitcherEnabled: Boolean,
+    ) {
+        checkNotNull(model)
+        assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
+        assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
+        assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
+    }
+
+    /**
+     * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
+     * is then automatically canceled and cleaned-up.
+     */
+    private fun runSelfCancelingTest(
+        block: suspend CoroutineScope.() -> Unit,
+    ) =
+        runBlocking(Dispatchers.Main.immediate) {
+            val scope = CoroutineScope(coroutineContext + Job())
+            block(scope)
+            scope.cancel()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 6fec343..dcea83a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,201 +17,54 @@
 
 package com.android.systemui.user.data.repository
 
-import android.content.pm.UserInfo
 import android.os.UserManager
-import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
+import com.android.systemui.util.settings.FakeSettings
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
-@SmallTest
-@RunWith(JUnit4::class)
-class UserRepositoryImplTest : SysuiTestCase() {
+abstract class UserRepositoryImplTest : SysuiTestCase() {
 
-    @Mock private lateinit var manager: UserManager
-    @Mock private lateinit var controller: UserSwitcherController
-    @Captor
-    private lateinit var userSwitchCallbackCaptor:
-        ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+    @Mock protected lateinit var manager: UserManager
+    @Mock protected lateinit var controller: UserSwitcherController
 
-    private lateinit var underTest: UserRepositoryImpl
+    protected lateinit var underTest: UserRepositoryImpl
 
-    @Before
-    fun setUp() {
+    protected lateinit var globalSettings: FakeSettings
+    protected lateinit var tracker: FakeUserTracker
+    protected lateinit var featureFlags: FakeFeatureFlags
+
+    protected fun setUp(isRefactored: Boolean) {
         MockitoAnnotations.initMocks(this)
-        whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
-        whenever(controller.isGuestUserAutoCreated).thenReturn(false)
-        whenever(controller.isGuestUserResetting).thenReturn(false)
 
-        underTest =
-            UserRepositoryImpl(
-                appContext = context,
-                manager = manager,
-                controller = controller,
-            )
+        globalSettings = FakeSettings()
+        tracker = FakeUserTracker()
+        featureFlags = FakeFeatureFlags()
+        featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
     }
 
-    @Test
-    fun `users - registers for updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.users.onEach {}.launchIn(this)
-
-            verify(controller).addUserSwitchCallback(any())
-
-            job.cancel()
-        }
-
-    @Test
-    fun `users - unregisters from updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.users.onEach {}.launchIn(this)
-            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
-            job.cancel()
-
-            verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
-        }
-
-    @Test
-    fun `users - does not include actions`() =
-        runBlocking(IMMEDIATE) {
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0, isSelected = true),
-                        createActionRecord(UserActionModel.ADD_USER),
-                        createUserRecord(1),
-                        createUserRecord(2),
-                        createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
-                        createActionRecord(UserActionModel.ENTER_GUEST_MODE),
-                    )
-                )
-            var models: List<UserModel>? = null
-            val job = underTest.users.onEach { models = it }.launchIn(this)
-
-            assertThat(models).hasSize(3)
-            assertThat(models?.get(0)?.id).isEqualTo(0)
-            assertThat(models?.get(0)?.isSelected).isTrue()
-            assertThat(models?.get(1)?.id).isEqualTo(1)
-            assertThat(models?.get(1)?.isSelected).isFalse()
-            assertThat(models?.get(2)?.id).isEqualTo(2)
-            assertThat(models?.get(2)?.isSelected).isFalse()
-            job.cancel()
-        }
-
-    @Test
-    fun selectedUser() =
-        runBlocking(IMMEDIATE) {
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0, isSelected = true),
-                        createUserRecord(1),
-                        createUserRecord(2),
-                    )
-                )
-            var id: Int? = null
-            val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
-
-            assertThat(id).isEqualTo(0)
-
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0),
-                        createUserRecord(1),
-                        createUserRecord(2, isSelected = true),
-                    )
-                )
-            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-            userSwitchCallbackCaptor.value.onUserSwitched()
-            assertThat(id).isEqualTo(2)
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - unregisters from updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.actions.onEach {}.launchIn(this)
-            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
-            job.cancel()
-
-            verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
-        }
-
-    @Test
-    fun `actions - registers for updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.actions.onEach {}.launchIn(this)
-
-            verify(controller).addUserSwitchCallback(any())
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actopms - does not include users`() =
-        runBlocking(IMMEDIATE) {
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0, isSelected = true),
-                        createActionRecord(UserActionModel.ADD_USER),
-                        createUserRecord(1),
-                        createUserRecord(2),
-                        createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
-                        createActionRecord(UserActionModel.ENTER_GUEST_MODE),
-                    )
-                )
-            var models: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { models = it }.launchIn(this)
-
-            assertThat(models).hasSize(3)
-            assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
-            assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
-            assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
-            job.cancel()
-        }
-
-    private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
-        return UserRecord(
-            info = UserInfo(id, "name$id", 0),
-            isCurrent = isSelected,
-        )
-    }
-
-    private fun createActionRecord(action: UserActionModel): UserRecord {
-        return UserRecord(
-            isAddUser = action == UserActionModel.ADD_USER,
-            isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
-            isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+    protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+        return UserRepositoryImpl(
+            appContext = context,
+            manager = manager,
+            controller = controller,
+            applicationScope = scope,
+            mainDispatcher = IMMEDIATE,
+            backgroundDispatcher = IMMEDIATE,
+            globalSettings = globalSettings,
+            tracker = tracker,
+            featureFlags = featureFlags,
         )
     }
 
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
+        @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
new file mode 100644
index 0000000..d4b41c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.user.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() {
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+
+    @Captor
+    private lateinit var userSwitchCallbackCaptor:
+        ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+
+    @Before
+    fun setUp() {
+        super.setUp(isRefactored = false)
+
+        whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
+        whenever(controller.isGuestUserAutoCreated).thenReturn(false)
+        whenever(controller.isGuestUserResetting).thenReturn(false)
+
+        underTest = create()
+    }
+
+    @Test
+    fun `users - registers for updates`() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.users.onEach {}.launchIn(this)
+
+            verify(controller).addUserSwitchCallback(any())
+
+            job.cancel()
+        }
+
+    @Test
+    fun `users - unregisters from updates`() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.users.onEach {}.launchIn(this)
+            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+            job.cancel()
+
+            verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+        }
+
+    @Test
+    fun `users - does not include actions`() =
+        runBlocking(IMMEDIATE) {
+            whenever(controller.users)
+                .thenReturn(
+                    arrayListOf(
+                        createUserRecord(0, isSelected = true),
+                        createActionRecord(UserActionModel.ADD_USER),
+                        createUserRecord(1),
+                        createUserRecord(2),
+                        createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+                        createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                    )
+                )
+            var models: List<UserModel>? = null
+            val job = underTest.users.onEach { models = it }.launchIn(this)
+
+            assertThat(models).hasSize(3)
+            assertThat(models?.get(0)?.id).isEqualTo(0)
+            assertThat(models?.get(0)?.isSelected).isTrue()
+            assertThat(models?.get(1)?.id).isEqualTo(1)
+            assertThat(models?.get(1)?.isSelected).isFalse()
+            assertThat(models?.get(2)?.id).isEqualTo(2)
+            assertThat(models?.get(2)?.isSelected).isFalse()
+            job.cancel()
+        }
+
+    @Test
+    fun selectedUser() =
+        runBlocking(IMMEDIATE) {
+            whenever(controller.users)
+                .thenReturn(
+                    arrayListOf(
+                        createUserRecord(0, isSelected = true),
+                        createUserRecord(1),
+                        createUserRecord(2),
+                    )
+                )
+            var id: Int? = null
+            val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
+
+            assertThat(id).isEqualTo(0)
+
+            whenever(controller.users)
+                .thenReturn(
+                    arrayListOf(
+                        createUserRecord(0),
+                        createUserRecord(1),
+                        createUserRecord(2, isSelected = true),
+                    )
+                )
+            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+            userSwitchCallbackCaptor.value.onUserSwitched()
+            assertThat(id).isEqualTo(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - unregisters from updates`() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.actions.onEach {}.launchIn(this)
+            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+            job.cancel()
+
+            verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+        }
+
+    @Test
+    fun `actions - registers for updates`() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.actions.onEach {}.launchIn(this)
+
+            verify(controller).addUserSwitchCallback(any())
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - does not include users`() =
+        runBlocking(IMMEDIATE) {
+            whenever(controller.users)
+                .thenReturn(
+                    arrayListOf(
+                        createUserRecord(0, isSelected = true),
+                        createActionRecord(UserActionModel.ADD_USER),
+                        createUserRecord(1),
+                        createUserRecord(2),
+                        createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+                        createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                    )
+                )
+            var models: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { models = it }.launchIn(this)
+
+            assertThat(models).hasSize(3)
+            assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
+            assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
+            assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+            job.cancel()
+        }
+
+    private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
+        return UserRecord(
+            info = UserInfo(id, "name$id", 0),
+            isCurrent = isSelected,
+        )
+    }
+
+    private fun createActionRecord(action: UserActionModel): UserRecord {
+        return UserRecord(
+            isAddUser = action == UserActionModel.ADD_USER,
+            isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
+            isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
new file mode 100644
index 0000000..120bf79
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -0,0 +1,394 @@
+/*
+ * 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.user.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
+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.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GuestUserInteractorTest : SysuiTestCase() {
+
+    @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit
+    @Mock private lateinit var dismissDialog: () -> Unit
+    @Mock private lateinit var selectUser: (Int) -> Unit
+    @Mock private lateinit var switchUser: (Int) -> Unit
+
+    private lateinit var underTest: GuestUserInteractor
+
+    private lateinit var scope: TestCoroutineScope
+    private lateinit var repository: FakeUserRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO)
+
+        scope = TestCoroutineScope()
+        repository = FakeUserRepository()
+        repository.setUserInfos(ALL_USERS)
+
+        underTest =
+            GuestUserInteractor(
+                applicationContext = context,
+                applicationScope = scope,
+                mainDispatcher = IMMEDIATE,
+                backgroundDispatcher = IMMEDIATE,
+                manager = manager,
+                repository = repository,
+                deviceProvisionedController = deviceProvisionedController,
+                devicePolicyManager = devicePolicyManager,
+                refreshUsersScheduler =
+                    RefreshUsersScheduler(
+                        applicationScope = scope,
+                        mainDispatcher = IMMEDIATE,
+                        repository = repository,
+                    ),
+                uiEventLogger = uiEventLogger,
+            )
+    }
+
+    @Test
+    fun `onDeviceBootCompleted - allowed to add - create guest`() =
+        runBlocking(IMMEDIATE) {
+            setAllowedToAdd()
+
+            underTest.onDeviceBootCompleted()
+
+            verify(manager).createGuest(any())
+            verify(deviceProvisionedController, never()).addCallback(any())
+        }
+
+    @Test
+    fun `onDeviceBootCompleted - await provisioning - and create guest`() =
+        runBlocking(IMMEDIATE) {
+            setAllowedToAdd(isAllowed = false)
+            underTest.onDeviceBootCompleted()
+            val captor =
+                kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>()
+            verify(deviceProvisionedController).addCallback(captor.capture())
+
+            setAllowedToAdd(isAllowed = true)
+            captor.value.onDeviceProvisionedChanged()
+
+            verify(manager).createGuest(any())
+            verify(deviceProvisionedController).removeCallback(captor.value)
+        }
+
+    @Test
+    fun createAndSwitchTo() =
+        runBlocking(IMMEDIATE) {
+            underTest.createAndSwitchTo(
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                selectUser = selectUser,
+            )
+
+            verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+            verify(manager).createGuest(any())
+            verify(dismissDialog).invoke()
+            verify(selectUser).invoke(GUEST_USER_INFO.id)
+        }
+
+    @Test
+    fun `createAndSwitchTo - fails to create - does not switch to`() =
+        runBlocking(IMMEDIATE) {
+            whenever(manager.createGuest(any())).thenReturn(null)
+
+            underTest.createAndSwitchTo(
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                selectUser = selectUser,
+            )
+
+            verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+            verify(manager).createGuest(any())
+            verify(dismissDialog).invoke()
+            verify(selectUser, never()).invoke(anyInt())
+        }
+
+    @Test
+    fun `exit - returns to target user`() =
+        runBlocking(IMMEDIATE) {
+            repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+            val targetUserId = NON_GUEST_USER_INFO.id
+            underTest.exit(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = targetUserId,
+                forceRemoveGuestOnExit = false,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verify(manager, never()).markGuestForDeletion(anyInt())
+            verify(manager, never()).removeUser(anyInt())
+            verify(switchUser).invoke(targetUserId)
+        }
+
+    @Test
+    fun `exit - returns to last non-guest`() =
+        runBlocking(IMMEDIATE) {
+            val expectedUserId = NON_GUEST_USER_INFO.id
+            whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO)
+            repository.lastSelectedNonGuestUserId = expectedUserId
+            repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+            underTest.exit(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = UserHandle.USER_NULL,
+                forceRemoveGuestOnExit = false,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verify(manager, never()).markGuestForDeletion(anyInt())
+            verify(manager, never()).removeUser(anyInt())
+            verify(switchUser).invoke(expectedUserId)
+        }
+
+    @Test
+    fun `exit - last non-guest was removed - returns to system`() =
+        runBlocking(IMMEDIATE) {
+            val removedUserId = 310
+            repository.lastSelectedNonGuestUserId = removedUserId
+            repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+            underTest.exit(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = UserHandle.USER_NULL,
+                forceRemoveGuestOnExit = false,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verify(manager, never()).markGuestForDeletion(anyInt())
+            verify(manager, never()).removeUser(anyInt())
+            verify(switchUser).invoke(UserHandle.USER_SYSTEM)
+        }
+
+    @Test
+    fun `exit - guest was ephemeral - it is removed`() =
+        runBlocking(IMMEDIATE) {
+            whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+            repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO))
+            repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO)
+            val targetUserId = NON_GUEST_USER_INFO.id
+
+            underTest.exit(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = targetUserId,
+                forceRemoveGuestOnExit = false,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id)
+            verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id)
+            verify(switchUser).invoke(targetUserId)
+        }
+
+    @Test
+    fun `exit - force remove guest - it is removed`() =
+        runBlocking(IMMEDIATE) {
+            whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+            repository.setSelectedUserInfo(GUEST_USER_INFO)
+            val targetUserId = NON_GUEST_USER_INFO.id
+
+            underTest.exit(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = targetUserId,
+                forceRemoveGuestOnExit = true,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+            verify(manager).removeUser(GUEST_USER_INFO.id)
+            verify(switchUser).invoke(targetUserId)
+        }
+
+    @Test
+    fun `exit - selected different from guest user - do nothing`() =
+        runBlocking(IMMEDIATE) {
+            repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+            underTest.exit(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = 123,
+                forceRemoveGuestOnExit = false,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verifyDidNotExit()
+        }
+
+    @Test
+    fun `exit - selected is actually not a guest user - do nothing`() =
+        runBlocking(IMMEDIATE) {
+            repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+            underTest.exit(
+                guestUserId = NON_GUEST_USER_INFO.id,
+                targetUserId = 123,
+                forceRemoveGuestOnExit = false,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verifyDidNotExit()
+        }
+
+    @Test
+    fun `remove - returns to target user`() =
+        runBlocking(IMMEDIATE) {
+            whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+            repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+            val targetUserId = NON_GUEST_USER_INFO.id
+            underTest.remove(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = targetUserId,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+            verify(manager).removeUser(GUEST_USER_INFO.id)
+            verify(switchUser).invoke(targetUserId)
+        }
+
+    @Test
+    fun `remove - selected different from guest user - do nothing`() =
+        runBlocking(IMMEDIATE) {
+            whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+            repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+            underTest.remove(
+                guestUserId = GUEST_USER_INFO.id,
+                targetUserId = 123,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verifyDidNotRemove()
+        }
+
+    @Test
+    fun `remove - selected is actually not a guest user - do nothing`() =
+        runBlocking(IMMEDIATE) {
+            whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+            repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+            underTest.remove(
+                guestUserId = NON_GUEST_USER_INFO.id,
+                targetUserId = 123,
+                showDialog = showDialog,
+                dismissDialog = dismissDialog,
+                switchUser = switchUser,
+            )
+
+            verifyDidNotRemove()
+        }
+
+    private fun setAllowedToAdd(isAllowed: Boolean = true) {
+        whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed)
+        whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed)
+    }
+
+    private fun verifyDidNotExit() {
+        verifyDidNotRemove()
+        verify(manager, never()).getUserInfo(anyInt())
+        verify(uiEventLogger, never()).log(any())
+    }
+
+    private fun verifyDidNotRemove() {
+        verify(manager, never()).markGuestForDeletion(anyInt())
+        verify(showDialog, never()).invoke(any())
+        verify(dismissDialog, never()).invoke()
+        verify(switchUser, never()).invoke(anyInt())
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private val NON_GUEST_USER_INFO =
+            UserInfo(
+                /* id= */ 818,
+                /* name= */ "non_guest",
+                /* flags= */ 0,
+            )
+        private val GUEST_USER_INFO =
+            UserInfo(
+                /* id= */ 669,
+                /* name= */ "guest",
+                /* iconPath= */ "",
+                /* flags= */ 0,
+                UserManager.USER_TYPE_FULL_GUEST,
+            )
+        private val EPHEMERAL_GUEST_USER_INFO =
+            UserInfo(
+                /* id= */ 669,
+                /* name= */ "guest",
+                /* iconPath= */ "",
+                /* flags= */ UserInfo.FLAG_EPHEMERAL,
+                UserManager.USER_TYPE_FULL_GUEST,
+            )
+        private val ALL_USERS =
+            listOf(
+                NON_GUEST_USER_INFO,
+                GUEST_USER_INFO,
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
new file mode 100644
index 0000000..593ce1f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RefreshUsersSchedulerTest : SysuiTestCase() {
+
+    private lateinit var underTest: RefreshUsersScheduler
+
+    private lateinit var repository: FakeUserRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        repository = FakeUserRepository()
+    }
+
+    @Test
+    fun `pause - prevents the next refresh from happening`() =
+        runBlocking(IMMEDIATE) {
+            underTest =
+                RefreshUsersScheduler(
+                    applicationScope = this,
+                    mainDispatcher = IMMEDIATE,
+                    repository = repository,
+                )
+            underTest.pause()
+
+            underTest.refreshIfNotPaused()
+            assertThat(repository.refreshUsersCallCount).isEqualTo(0)
+        }
+
+    @Test
+    fun `unpauseAndRefresh - forces the refresh even when paused`() =
+        runBlocking(IMMEDIATE) {
+            underTest =
+                RefreshUsersScheduler(
+                    applicationScope = this,
+                    mainDispatcher = IMMEDIATE,
+                    repository = repository,
+                )
+            underTest.pause()
+
+            underTest.unpauseAndRefresh()
+
+            assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+        }
+
+    @Test
+    fun `refreshIfNotPaused - refreshes when not paused`() =
+        runBlocking(IMMEDIATE) {
+            underTest =
+                RefreshUsersScheduler(
+                    applicationScope = this,
+                    mainDispatcher = IMMEDIATE,
+                    repository = repository,
+                )
+            underTest.refreshIfNotPaused()
+
+            assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
new file mode 100644
index 0000000..3d5695a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -0,0 +1,658 @@
+/*
+ * 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.user.domain.interactor
+
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.internal.R.drawable.ic_account_circle
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+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.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserInteractorRefactoredTest : UserInteractorTest() {
+
+    override fun isRefactored(): Boolean {
+        return true
+    }
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+
+        overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
+        overrideResource(R.dimen.max_avatar_size, 10)
+        overrideResource(
+            com.android.internal.R.string.config_supervisedUserCreationPackage,
+            SUPERVISED_USER_CREATION_APP_PACKAGE,
+        )
+        whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
+        whenever(manager.canAddMoreUsers(any())).thenReturn(true)
+    }
+
+    @Test
+    fun `users - switcher enabled`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var value: List<UserModel>? = null
+            val job = underTest.users.onEach { value = it }.launchIn(this)
+            assertUsers(models = value, count = 3, includeGuest = true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `users - switches to second user`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var value: List<UserModel>? = null
+            val job = underTest.users.onEach { value = it }.launchIn(this)
+            userRepository.setSelectedUserInfo(userInfos[1])
+
+            assertUsers(models = value, count = 2, selectedIndex = 1)
+            job.cancel()
+        }
+
+    @Test
+    fun `users - switcher not enabled`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+
+            var value: List<UserModel>? = null
+            val job = underTest.users.onEach { value = it }.launchIn(this)
+            assertUsers(models = value, count = 1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun selectedUser() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var value: UserModel? = null
+            val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
+            assertUser(value, id = 0, isSelected = true)
+
+            userRepository.setSelectedUserInfo(userInfos[1])
+            assertUser(value, id = 1, isSelected = true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device unlocked`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device unlocked user not primary - empty list`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device unlocked user is guest - empty list`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = true)
+            assertThat(userInfos[1].isGuest).isTrue()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device locked add from lockscreen set - full list`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(
+                UserSwitcherSettingsModel(
+                    isUserSwitcherEnabled = true,
+                    isAddUsersFromLockscreen = true,
+                )
+            )
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device locked - only guest action is shown`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(true)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE))
+
+            job.cancel()
+        }
+
+    @Test
+    fun `executeAction - add user - dialog shown`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            keyguardRepository.setKeyguardShowing(false)
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.executeAction(UserActionModel.ADD_USER)
+            assertThat(dialogRequest)
+                .isEqualTo(
+                    ShowDialogRequestModel.ShowAddUserDialog(
+                        userHandle = userInfos[0].userHandle,
+                        isKeyguardShowing = false,
+                        showEphemeralMessage = false,
+                    )
+                )
+
+            underTest.onDialogShown()
+            assertThat(dialogRequest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `executeAction - add supervised user - starts activity`() =
+        runBlocking(IMMEDIATE) {
+            underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+            val intentCaptor = kotlinArgumentCaptor<Intent>()
+            verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+            assertThat(intentCaptor.value.action)
+                .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
+            assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
+        }
+
+    @Test
+    fun `executeAction - navigate to manage users`() =
+        runBlocking(IMMEDIATE) {
+            underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+            val intentCaptor = kotlinArgumentCaptor<Intent>()
+            verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+            assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+        }
+
+    @Test
+    fun `executeAction - guest mode`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
+            whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
+            val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
+            val showDialogsJob =
+                underTest.dialogShowRequests
+                    .onEach {
+                        dialogRequests.add(it)
+                        if (it != null) {
+                            underTest.onDialogShown()
+                        }
+                    }
+                    .launchIn(this)
+            val dismissDialogsJob =
+                underTest.dialogDismissRequests
+                    .onEach {
+                        if (it != null) {
+                            underTest.onDialogDismissed()
+                        }
+                    }
+                    .launchIn(this)
+
+            underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+            assertThat(dialogRequests)
+                .contains(
+                    ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
+                )
+            verify(activityManager).switchUser(guestUserInfo.id)
+
+            showDialogsJob.cancel()
+            dismissDialogsJob.cancel()
+        }
+
+    @Test
+    fun `selectUser - already selected guest re-selected - exit guest dialog`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = true)
+            val guestUserInfo = userInfos[1]
+            assertThat(guestUserInfo.isGuest).isTrue()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(guestUserInfo)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.selectUser(newlySelectedUserId = guestUserInfo.id)
+
+            assertThat(dialogRequest)
+                .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+            job.cancel()
+        }
+
+    @Test
+    fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = true)
+            val guestUserInfo = userInfos[1]
+            assertThat(guestUserInfo.isGuest).isTrue()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(guestUserInfo)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.selectUser(newlySelectedUserId = userInfos[0].id)
+
+            assertThat(dialogRequest)
+                .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+            job.cancel()
+        }
+
+    @Test
+    fun `selectUser - not currently guest - switches users`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.selectUser(newlySelectedUserId = userInfos[1].id)
+
+            assertThat(dialogRequest).isNull()
+            verify(activityManager).switchUser(userInfos[1].id)
+            job.cancel()
+        }
+
+    @Test
+    fun `Telephony call state changes - refreshes users`() =
+        runBlocking(IMMEDIATE) {
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            telephonyRepository.setCallState(1)
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `User switched broadcast`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            val callback1: UserInteractor.UserCallback = mock()
+            val callback2: UserInteractor.UserCallback = mock()
+            underTest.addCallback(callback1)
+            underTest.addCallback(callback2)
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            userRepository.setSelectedUserInfo(userInfos[1])
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_SWITCHED)
+                        .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
+                )
+            }
+
+            verify(callback1).onUserStateChanged()
+            verify(callback2).onUserStateChanged()
+            assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `User info changed broadcast`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_INFO_CHANGED),
+                )
+            }
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `System user unlocked broadcast - refresh users`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_UNLOCKED)
+                        .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
+                )
+            }
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `Non-system user unlocked broadcast - do not refresh users`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
+                )
+            }
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
+        }
+
+    @Test
+    fun userRecords() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = false)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            keyguardRepository.setKeyguardShowing(false)
+
+            testCoroutineScope.advanceUntilIdle()
+
+            assertRecords(
+                records = underTest.userRecords.value,
+                userIds = listOf(0, 1, 2),
+                selectedUserIndex = 0,
+                includeGuest = false,
+                expectedActions =
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                    ),
+            )
+        }
+
+    @Test
+    fun selectedUserRecord() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            keyguardRepository.setKeyguardShowing(false)
+
+            assertRecordForUser(
+                record = underTest.selectedUserRecord.value,
+                id = 0,
+                hasPicture = true,
+                isCurrent = true,
+                isSwitchToEnabled = true,
+            )
+        }
+
+    private fun assertUsers(
+        models: List<UserModel>?,
+        count: Int,
+        selectedIndex: Int = 0,
+        includeGuest: Boolean = false,
+    ) {
+        checkNotNull(models)
+        assertThat(models.size).isEqualTo(count)
+        models.forEachIndexed { index, model ->
+            assertUser(
+                model = model,
+                id = index,
+                isSelected = index == selectedIndex,
+                isGuest = includeGuest && index == count - 1
+            )
+        }
+    }
+
+    private fun assertUser(
+        model: UserModel?,
+        id: Int,
+        isSelected: Boolean = false,
+        isGuest: Boolean = false,
+    ) {
+        checkNotNull(model)
+        assertThat(model.id).isEqualTo(id)
+        assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
+        assertThat(model.isSelected).isEqualTo(isSelected)
+        assertThat(model.isSelectable).isTrue()
+        assertThat(model.isGuest).isEqualTo(isGuest)
+    }
+
+    private fun assertRecords(
+        records: List<UserRecord>,
+        userIds: List<Int>,
+        selectedUserIndex: Int = 0,
+        includeGuest: Boolean = false,
+        expectedActions: List<UserActionModel> = emptyList(),
+    ) {
+        assertThat(records.size >= userIds.size).isTrue()
+        userIds.indices.forEach { userIndex ->
+            val record = records[userIndex]
+            assertThat(record.info).isNotNull()
+            val isGuest = includeGuest && userIndex == userIds.size - 1
+            assertRecordForUser(
+                record = record,
+                id = userIds[userIndex],
+                hasPicture = !isGuest,
+                isCurrent = userIndex == selectedUserIndex,
+                isGuest = isGuest,
+                isSwitchToEnabled = true,
+            )
+        }
+
+        assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
+        (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
+            val record = records[actionIndex]
+            assertThat(record.info).isNull()
+            assertRecordForAction(
+                record = record,
+                type = expectedActions[actionIndex - userIds.size],
+            )
+        }
+    }
+
+    private fun assertRecordForUser(
+        record: UserRecord?,
+        id: Int? = null,
+        hasPicture: Boolean = false,
+        isCurrent: Boolean = false,
+        isGuest: Boolean = false,
+        isSwitchToEnabled: Boolean = false,
+    ) {
+        checkNotNull(record)
+        assertThat(record.info?.id).isEqualTo(id)
+        assertThat(record.picture != null).isEqualTo(hasPicture)
+        assertThat(record.isCurrent).isEqualTo(isCurrent)
+        assertThat(record.isGuest).isEqualTo(isGuest)
+        assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
+    }
+
+    private fun assertRecordForAction(
+        record: UserRecord,
+        type: UserActionModel,
+    ) {
+        assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
+        assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
+        assertThat(record.isAddSupervisedUser)
+            .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+    }
+
+    private fun createUserInfos(
+        count: Int,
+        includeGuest: Boolean,
+    ): List<UserInfo> {
+        return (0 until count).map { index ->
+            val isGuest = includeGuest && index == count - 1
+            createUserInfo(
+                id = index,
+                name =
+                    if (isGuest) {
+                        "guest"
+                    } else {
+                        "user_$index"
+                    },
+                isPrimary = !isGuest && index == 0,
+                isGuest = isGuest,
+            )
+        }
+    }
+
+    private fun createUserInfo(
+        id: Int,
+        name: String,
+        isPrimary: Boolean = false,
+        isGuest: Boolean = false,
+    ): UserInfo {
+        return UserInfo(
+            id,
+            name,
+            /* iconPath= */ "",
+            /* flags= */ if (isPrimary) {
+                UserInfo.FLAG_PRIMARY
+            } else {
+                0
+            },
+            if (isGuest) {
+                UserManager.USER_TYPE_FULL_GUEST
+            } else {
+                UserManager.USER_TYPE_FULL_SYSTEM
+            },
+        )
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+        private val GUEST_ICON: Drawable = mock()
+        private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index e914e2e..8465f4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -17,51 +17,61 @@
 
 package com.android.systemui.user.domain.interactor
 
-import androidx.test.filters.SmallTest
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.os.UserManager
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-@SmallTest
-@RunWith(JUnit4::class)
-class UserInteractorTest : SysuiTestCase() {
+abstract class UserInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var controller: UserSwitcherController
-    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock protected lateinit var controller: UserSwitcherController
+    @Mock protected lateinit var activityStarter: ActivityStarter
+    @Mock protected lateinit var manager: UserManager
+    @Mock protected lateinit var activityManager: ActivityManager
+    @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock protected lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock protected lateinit var uiEventLogger: UiEventLogger
 
-    private lateinit var underTest: UserInteractor
+    protected lateinit var underTest: UserInteractor
 
-    private lateinit var userRepository: FakeUserRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
+    protected lateinit var testCoroutineScope: TestCoroutineScope
+    protected lateinit var userRepository: FakeUserRepository
+    protected lateinit var keyguardRepository: FakeKeyguardRepository
+    protected lateinit var telephonyRepository: FakeTelephonyRepository
 
-    @Before
-    fun setUp() {
+    abstract fun isRefactored(): Boolean
+
+    open fun setUp() {
         MockitoAnnotations.initMocks(this)
 
         userRepository = FakeUserRepository()
         keyguardRepository = FakeKeyguardRepository()
+        telephonyRepository = FakeTelephonyRepository()
+        testCoroutineScope = TestCoroutineScope()
+        val refreshUsersScheduler =
+            RefreshUsersScheduler(
+                applicationScope = testCoroutineScope,
+                mainDispatcher = IMMEDIATE,
+                repository = userRepository,
+            )
         underTest =
             UserInteractor(
+                applicationContext = context,
                 repository = userRepository,
                 controller = controller,
                 activityStarter = activityStarter,
@@ -69,142 +79,34 @@
                     KeyguardInteractor(
                         repository = keyguardRepository,
                     ),
-            )
-    }
-
-    @Test
-    fun `actions - not actionable when locked and locked - no actions`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(UserActionModel.values().toList())
-            userRepository.setActionableWhenLocked(false)
-            keyguardRepository.setKeyguardShowing(true)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions).isEmpty()
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - not actionable when locked and not locked`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
-            userRepository.setActionableWhenLocked(false)
-            keyguardRepository.setKeyguardShowing(false)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                featureFlags =
+                    FakeFeatureFlags().apply {
+                        set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
+                    },
+                manager = manager,
+                applicationScope = testCoroutineScope,
+                telephonyInteractor =
+                    TelephonyInteractor(
+                        repository = telephonyRepository,
+                    ),
+                broadcastDispatcher = fakeBroadcastDispatcher,
+                backgroundDispatcher = IMMEDIATE,
+                activityManager = activityManager,
+                refreshUsersScheduler = refreshUsersScheduler,
+                guestUserInteractor =
+                    GuestUserInteractor(
+                        applicationContext = context,
+                        applicationScope = testCoroutineScope,
+                        mainDispatcher = IMMEDIATE,
+                        backgroundDispatcher = IMMEDIATE,
+                        manager = manager,
+                        repository = userRepository,
+                        deviceProvisionedController = deviceProvisionedController,
+                        devicePolicyManager = devicePolicyManager,
+                        refreshUsersScheduler = refreshUsersScheduler,
+                        uiEventLogger = uiEventLogger,
                     )
-                )
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - actionable when locked and not locked`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
             )
-            userRepository.setActionableWhenLocked(true)
-            keyguardRepository.setKeyguardShowing(false)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - actionable when locked and locked`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
-            userRepository.setActionableWhenLocked(true)
-            keyguardRepository.setKeyguardShowing(true)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-            job.cancel()
-        }
-
-    @Test
-    fun selectUser() {
-        val userId = 3
-
-        underTest.selectUser(userId)
-
-        verify(controller).onUserSelected(eq(userId), nullable())
-    }
-
-    @Test
-    fun `executeAction - guest`() {
-        underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
-
-        verify(controller).createAndSwitchToGuestUser(nullable())
-    }
-
-    @Test
-    fun `executeAction - add user`() {
-        underTest.executeAction(UserActionModel.ADD_USER)
-
-        verify(controller).showAddUserDialog(nullable())
-    }
-
-    @Test
-    fun `executeAction - add supervised user`() {
-        underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
-
-        verify(controller).startSupervisedUserActivity()
-    }
-
-    @Test
-    fun `executeAction - manage users`() {
-        underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-
-        verify(activityStarter).startActivity(any(), anyBoolean())
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
new file mode 100644
index 0000000..c3a9705
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+open class UserInteractorUnrefactoredTest : UserInteractorTest() {
+
+    override fun isRefactored(): Boolean {
+        return false
+    }
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+    }
+
+    @Test
+    fun `actions - not actionable when locked and locked - no actions`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(UserActionModel.values().toList())
+            userRepository.setActionableWhenLocked(false)
+            keyguardRepository.setKeyguardShowing(true)
+
+            var actions: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions).isEmpty()
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - not actionable when locked and not locked`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(
+                listOf(
+                    UserActionModel.ENTER_GUEST_MODE,
+                    UserActionModel.ADD_USER,
+                    UserActionModel.ADD_SUPERVISED_USER,
+                )
+            )
+            userRepository.setActionableWhenLocked(false)
+            keyguardRepository.setKeyguardShowing(false)
+
+            var actions: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - actionable when locked and not locked`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(
+                listOf(
+                    UserActionModel.ENTER_GUEST_MODE,
+                    UserActionModel.ADD_USER,
+                    UserActionModel.ADD_SUPERVISED_USER,
+                )
+            )
+            userRepository.setActionableWhenLocked(true)
+            keyguardRepository.setKeyguardShowing(false)
+
+            var actions: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - actionable when locked and locked`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(
+                listOf(
+                    UserActionModel.ENTER_GUEST_MODE,
+                    UserActionModel.ADD_USER,
+                    UserActionModel.ADD_SUPERVISED_USER,
+                )
+            )
+            userRepository.setActionableWhenLocked(true)
+            keyguardRepository.setKeyguardShowing(true)
+
+            var actions: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+            job.cancel()
+        }
+
+    @Test
+    fun selectUser() {
+        val userId = 3
+
+        underTest.selectUser(userId)
+
+        verify(controller).onUserSelected(eq(userId), nullable())
+    }
+
+    @Test
+    fun `executeAction - guest`() {
+        underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+        verify(controller).createAndSwitchToGuestUser(nullable())
+    }
+
+    @Test
+    fun `executeAction - add user`() {
+        underTest.executeAction(UserActionModel.ADD_USER)
+
+        verify(controller).showAddUserDialog(nullable())
+    }
+
+    @Test
+    fun `executeAction - add supervised user`() {
+        underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+        verify(controller).startSupervisedUserActivity()
+    }
+
+    @Test
+    fun `executeAction - manage users`() {
+        underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+        verify(activityStarter).startActivity(any(), anyBoolean())
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index ef4500d..0344e3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -17,17 +17,28 @@
 
 package com.android.systemui.user.ui.viewmodel
 
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
 import android.graphics.drawable.Drawable
+import android.os.UserManager
 import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import com.android.systemui.user.shared.model.UserActionModel
@@ -38,6 +49,7 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
 import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
@@ -52,6 +64,11 @@
 
     @Mock private lateinit var controller: UserSwitcherController
     @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var activityManager: ActivityManager
+    @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var uiEventLogger: UiEventLogger
 
     private lateinit var underTest: UserSwitcherViewModel
 
@@ -66,22 +83,60 @@
         userRepository = FakeUserRepository()
         keyguardRepository = FakeKeyguardRepository()
         powerRepository = FakePowerRepository()
+        val featureFlags = FakeFeatureFlags()
+        featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
+        val scope = TestCoroutineScope()
+        val refreshUsersScheduler =
+            RefreshUsersScheduler(
+                applicationScope = scope,
+                mainDispatcher = IMMEDIATE,
+                repository = userRepository,
+            )
+        val guestUserInteractor =
+            GuestUserInteractor(
+                applicationContext = context,
+                applicationScope = scope,
+                mainDispatcher = IMMEDIATE,
+                backgroundDispatcher = IMMEDIATE,
+                manager = manager,
+                repository = userRepository,
+                deviceProvisionedController = deviceProvisionedController,
+                devicePolicyManager = devicePolicyManager,
+                refreshUsersScheduler = refreshUsersScheduler,
+                uiEventLogger = uiEventLogger,
+            )
+
         underTest =
             UserSwitcherViewModel.Factory(
                     userInteractor =
                         UserInteractor(
+                            applicationContext = context,
                             repository = userRepository,
                             controller = controller,
                             activityStarter = activityStarter,
                             keyguardInteractor =
                                 KeyguardInteractor(
                                     repository = keyguardRepository,
-                                )
+                                ),
+                            featureFlags = featureFlags,
+                            manager = manager,
+                            applicationScope = scope,
+                            telephonyInteractor =
+                                TelephonyInteractor(
+                                    repository = FakeTelephonyRepository(),
+                                ),
+                            broadcastDispatcher = fakeBroadcastDispatcher,
+                            backgroundDispatcher = IMMEDIATE,
+                            activityManager = activityManager,
+                            refreshUsersScheduler = refreshUsersScheduler,
+                            guestUserInteractor = guestUserInteractor,
                         ),
                     powerInteractor =
                         PowerInteractor(
                             repository = powerRepository,
                         ),
+                    featureFlags = featureFlags,
+                    guestUserInteractor = guestUserInteractor,
                 )
                 .create(UserSwitcherViewModel::class.java)
     }
@@ -97,6 +152,7 @@
                         image = USER_IMAGE,
                         isSelected = true,
                         isSelectable = true,
+                        isGuest = false,
                     ),
                     UserModel(
                         id = 1,
@@ -104,6 +160,7 @@
                         image = USER_IMAGE,
                         isSelected = false,
                         isSelectable = true,
+                        isGuest = false,
                     ),
                     UserModel(
                         id = 2,
@@ -111,6 +168,7 @@
                         image = USER_IMAGE,
                         isSelected = false,
                         isSelectable = false,
+                        isGuest = false,
                     ),
                 )
             )
@@ -260,7 +318,7 @@
             job.cancel()
         }
 
-    private fun setUsers(count: Int) {
+    private suspend fun setUsers(count: Int) {
         userRepository.setUsers(
             (0 until count).map { index ->
                 UserModel(
@@ -269,6 +327,7 @@
                     image = USER_IMAGE,
                     isSelected = index == 0,
                     isSelectable = true,
+                    isGuest = false,
                 )
             }
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
index 53dcc8d..bb646f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
@@ -37,10 +37,18 @@
     dumpManager: DumpManager,
     logger: BroadcastDispatcherLogger,
     userTracker: UserTracker
-) : BroadcastDispatcher(
-    context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) {
+) :
+    BroadcastDispatcher(
+        context,
+        looper,
+        executor,
+        dumpManager,
+        logger,
+        userTracker,
+        PendingRemovalStore(logger)
+    ) {
 
-    private val registeredReceivers = ArraySet<BroadcastReceiver>()
+    val registeredReceivers = ArraySet<BroadcastReceiver>()
 
     override fun registerReceiverWithHandler(
         receiver: BroadcastReceiver,
@@ -78,4 +86,4 @@
         }
         registeredReceivers.clear()
     }
-}
\ No newline at end of file
+}
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 42b434a..725b1f4 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
@@ -44,6 +44,10 @@
     private val _dozeAmount = MutableStateFlow(0f)
     override val dozeAmount: Flow<Float> = _dozeAmount
 
+    override fun isKeyguardShowing(): Boolean {
+        return _isKeyguardShowing.value
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.tryEmit(animate)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index b2b1764..9726bf8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -26,20 +26,24 @@
 
 /** A fake [UserTracker] to be used in tests. */
 class FakeUserTracker(
-    userId: Int = 0,
-    userHandle: UserHandle = UserHandle.of(userId),
-    userInfo: UserInfo = mock(),
-    userProfiles: List<UserInfo> = emptyList(),
+    private var _userId: Int = 0,
+    private var _userHandle: UserHandle = UserHandle.of(_userId),
+    private var _userInfo: UserInfo = mock(),
+    private var _userProfiles: List<UserInfo> = emptyList(),
     userContentResolver: ContentResolver = MockContentResolver(),
     userContext: Context = mock(),
     private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
 ) : UserTracker {
     val callbacks = mutableListOf<UserTracker.Callback>()
 
-    override val userId: Int = userId
-    override val userHandle: UserHandle = userHandle
-    override val userInfo: UserInfo = userInfo
-    override val userProfiles: List<UserInfo> = userProfiles
+    override val userId: Int
+        get() = _userId
+    override val userHandle: UserHandle
+        get() = _userHandle
+    override val userInfo: UserInfo
+        get() = _userInfo
+    override val userProfiles: List<UserInfo>
+        get() = _userProfiles
 
     override val userContentResolver: ContentResolver = userContentResolver
     override val userContext: Context = userContext
@@ -55,4 +59,13 @@
     override fun createCurrentUserContext(context: Context): Context {
         return onCreateCurrentUserContext(context)
     }
+
+    fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) {
+        _userProfiles = userInfos
+        _userInfo = userInfos[selectedUserIndex]
+        _userId = _userInfo.id
+        _userHandle = UserHandle.of(_userId)
+
+        callbacks.forEach { it.onUserChanged(_userId, userContext) }
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
new file mode 100644
index 0000000..59f24ef
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.telephony.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeTelephonyRepository : TelephonyRepository {
+
+    private val _callState = MutableStateFlow(0)
+    override val callState: Flow<Int> = _callState.asStateFlow()
+
+    fun setCallState(value: Int) {
+        _callState.value = value
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 20f1e36..4df8aa4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -17,12 +17,18 @@
 
 package com.android.systemui.user.data.repository
 
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.shared.model.UserModel
+import java.util.concurrent.atomic.AtomicBoolean
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.yield
 
 class FakeUserRepository : UserRepository {
 
@@ -34,21 +40,71 @@
     private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
     override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
 
+    private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
+    override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+        _userSwitcherSettings.asStateFlow()
+
+    private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
+    override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
+
+    private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+    override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+    override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+
     private val _isActionableWhenLocked = MutableStateFlow(false)
     override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
 
     private var _isGuestUserAutoCreated: Boolean = false
     override val isGuestUserAutoCreated: Boolean
         get() = _isGuestUserAutoCreated
-    private var _isGuestUserResetting: Boolean = false
-    override val isGuestUserResetting: Boolean
-        get() = _isGuestUserResetting
+
+    override var isGuestUserResetting: Boolean = false
+
+    override val isGuestUserCreationScheduled = AtomicBoolean()
+
+    override var secondaryUserId: Int = UserHandle.USER_NULL
+
+    override var isRefreshUsersPaused: Boolean = false
+
+    var refreshUsersCallCount: Int = 0
+        private set
+
+    override fun refreshUsers() {
+        refreshUsersCallCount++
+    }
+
+    override fun getSelectedUserInfo(): UserInfo {
+        return checkNotNull(_selectedUserInfo.value)
+    }
+
+    override fun isSimpleUserSwitcher(): Boolean {
+        return _userSwitcherSettings.value.isSimpleUserSwitcher
+    }
+
+    fun setUserInfos(infos: List<UserInfo>) {
+        _userInfos.value = infos
+    }
+
+    suspend fun setSelectedUserInfo(userInfo: UserInfo) {
+        check(_userInfos.value.contains(userInfo)) {
+            "Cannot select the following user, it is not in the list of user infos: $userInfo!"
+        }
+
+        _selectedUserInfo.value = userInfo
+        yield()
+    }
+
+    suspend fun setSettings(settings: UserSwitcherSettingsModel) {
+        _userSwitcherSettings.value = settings
+        yield()
+    }
 
     fun setUsers(models: List<UserModel>) {
         _users.value = models
     }
 
-    fun setSelectedUser(userId: Int) {
+    suspend fun setSelectedUser(userId: Int) {
         check(_users.value.find { it.id == userId } != null) {
             "Cannot select a user with ID $userId - no user with that ID found!"
         }
@@ -62,6 +118,7 @@
                 }
             }
         )
+        yield()
     }
 
     fun setActions(models: List<UserActionModel>) {
@@ -75,8 +132,4 @@
     fun setGuestUserAutoCreated(value: Boolean) {
         _isGuestUserAutoCreated = value
     }
-
-    fun setGuestUserResetting(value: Boolean) {
-        _isGuestUserResetting = value
-    }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 56a5a8f..0426c79 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -1175,6 +1175,9 @@
     }
 
     private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+        if (packageInfo == null) {
+            return;
+        }
         if (containsEither(packageInfo.requestedPermissions,
                 android.Manifest.permission.RUN_IN_BACKGROUND,
                 android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index 9bad45b..3ab4aa8 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -54,12 +54,19 @@
     private static final String PROPERTY_PRIMARY_TAG =
             "android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE";
 
-    static @Nullable PackageInfo getPackageInfo(@NonNull Context context,
+    @Nullable
+    static PackageInfo getPackageInfo(@NonNull Context context,
             @UserIdInt int userId, @NonNull String packageName) {
         final PackageManager pm = context.getPackageManager();
         final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS);
-        return Binder.withCleanCallingIdentity(() ->
-                pm.getPackageInfoAsUser(packageName, flags , userId));
+        return Binder.withCleanCallingIdentity(() -> {
+            try {
+                return pm.getPackageInfoAsUser(packageName, flags, userId);
+            } catch (PackageManager.NameNotFoundException e) {
+                Slog.e(TAG, "Package [" + packageName + "] is not found.");
+                return null;
+            }
+        });
     }
 
     static void enforceUsesCompanionDeviceFeature(@NonNull Context context,
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 4204162..5f27f59 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -95,6 +95,7 @@
     private final AssociationInfo mAssociationInfo;
     private final PendingTrampolineCallback mPendingTrampolineCallback;
     private final int mOwnerUid;
+    private final int mDeviceId;
     private final InputController mInputController;
     private VirtualAudioController mVirtualAudioController;
     @VisibleForTesting
@@ -140,19 +141,40 @@
     private final SparseArray<GenericWindowPolicyController> mWindowPolicyControllers =
             new SparseArray<>();
 
-    VirtualDeviceImpl(Context context, AssociationInfo associationInfo,
-            IBinder token, int ownerUid, OnDeviceCloseListener listener,
+    VirtualDeviceImpl(
+            Context context,
+            AssociationInfo associationInfo,
+            IBinder token,
+            int ownerUid,
+            int deviceId,
+            OnDeviceCloseListener listener,
             PendingTrampolineCallback pendingTrampolineCallback,
             IVirtualDeviceActivityListener activityListener,
             Consumer<ArraySet<Integer>> runningAppsChangedCallback,
             VirtualDeviceParams params) {
-        this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener,
-                pendingTrampolineCallback, activityListener, runningAppsChangedCallback, params);
+        this(
+                context,
+                associationInfo,
+                token,
+                ownerUid,
+                deviceId,
+                /* inputController= */ null,
+                listener,
+                pendingTrampolineCallback,
+                activityListener,
+                runningAppsChangedCallback,
+                params);
     }
 
     @VisibleForTesting
-    VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token,
-            int ownerUid, InputController inputController, OnDeviceCloseListener listener,
+    VirtualDeviceImpl(
+            Context context,
+            AssociationInfo associationInfo,
+            IBinder token,
+            int ownerUid,
+            int deviceId,
+            InputController inputController,
+            OnDeviceCloseListener listener,
             PendingTrampolineCallback pendingTrampolineCallback,
             IVirtualDeviceActivityListener activityListener,
             Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -164,6 +186,7 @@
         mActivityListener = activityListener;
         mRunningAppsChangedCallback = runningAppsChangedCallback;
         mOwnerUid = ownerUid;
+        mDeviceId = deviceId;
         mAppToken = token;
         mParams = params;
         if (inputController == null) {
@@ -199,6 +222,12 @@
         return mAssociationInfo.getDisplayName();
     }
 
+    /** Returns the unique device ID of this device. */
+    @Override // Binder call
+    public int getDeviceId() {
+        return mDeviceId;
+    }
+
     @Override // Binder call
     public int getAssociationId() {
         return mAssociationInfo.getId();
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 2b644fe..06dfeab 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -60,12 +60,12 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 
 @SuppressLint("LongLogTag")
 public class VirtualDeviceManagerService extends SystemService {
 
-    private static final boolean DEBUG = false;
     private static final String TAG = "VirtualDeviceManagerService";
 
     private final Object mVirtualDeviceManagerLock = new Object();
@@ -73,6 +73,10 @@
     private final VirtualDeviceManagerInternal mLocalService;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler);
+
+    private static AtomicInteger sNextUniqueIndex = new AtomicInteger(
+            VirtualDeviceManager.DEFAULT_DEVICE_ID + 1);
+
     /**
      * Mapping from user IDs to CameraAccessControllers.
      */
@@ -260,8 +264,10 @@
                 final int userId = UserHandle.getUserId(callingUid);
                 final CameraAccessController cameraAccessController =
                         mCameraAccessControllers.get(userId);
+                final int uniqueId = sNextUniqueIndex.getAndIncrement();
+
                 VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(),
-                        associationInfo, token, callingUid,
+                        associationInfo, token, callingUid, uniqueId,
                         new VirtualDeviceImpl.OnDeviceCloseListener() {
                             @Override
                             public void onClose(int associationId) {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 5b1f740..83d527e 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -147,7 +147,6 @@
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
-import com.android.internal.widget.LockPatternUtils;
 import com.android.server.pm.Installer;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.storage.AppFuseBridge;
@@ -557,7 +556,6 @@
     private IAppOpsService mIAppOpsService;
 
     private final Callbacks mCallbacks;
-    private final LockPatternUtils mLockPatternUtils;
 
     private static final String ANR_DELAY_MILLIS_DEVICE_CONFIG_KEY =
             "anr_delay_millis";
@@ -1808,7 +1806,6 @@
                 ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false);
         mContext = context;
         mCallbacks = new Callbacks(FgThread.get().getLooper());
-        mLockPatternUtils = new LockPatternUtils(mContext);
 
         HandlerThread hthread = new HandlerThread(TAG);
         hthread.start();
@@ -3069,96 +3066,21 @@
         }
     }
 
-    private String encodeBytes(byte[] bytes) {
-        if (ArrayUtils.isEmpty(bytes)) {
-            return "!";
-        } else {
-            return HexDump.toHexString(bytes);
-        }
-    }
-
+    /* Only for use by LockSettingsService */
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
-    /*
-     * Add this secret to the set of ways we can recover a user's disk
-     * encryption key.  Changing the secret for a disk encryption key is done in
-     * two phases.  First, this method is called to add the new secret binding.
-     * Second, fixateNewestUserKeyAuth is called to delete all other bindings.
-     * This allows other places where a credential is used, such as Gatekeeper,
-     * to be updated between the two calls.
-     */
     @Override
-    public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-
-        try {
-            mVold.addUserKeyAuth(userId, serialNumber, encodeBytes(secret));
-        } catch (Exception e) {
-            Slog.wtf(TAG, e);
-        }
+    public void setUserKeyProtection(@UserIdInt int userId, byte[] secret) throws RemoteException {
+        mVold.setUserKeyProtection(userId, HexDump.toHexString(secret));
     }
 
+    /* Only for use by LockSettingsService */
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
-    /*
-     * Store a user's disk encryption key without secret binding.  Removing the
-     * secret for a disk encryption key is done in two phases.  First, this
-     * method is called to retrieve the key using the provided secret and store
-     * it encrypted with a keystore key not bound to the user.  Second,
-     * fixateNewestUserKeyAuth is called to delete the key's other bindings.
-     */
     @Override
-    public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-
-        try {
-            mVold.clearUserKeyAuth(userId, serialNumber, encodeBytes(secret));
-        } catch (Exception e) {
-            Slog.wtf(TAG, e);
+    public void unlockUserKey(@UserIdInt int userId, int serialNumber, byte[] secret)
+        throws RemoteException {
+        if (StorageManager.isFileEncrypted()) {
+            mVold.unlockUserKey(userId, serialNumber, HexDump.toHexString(secret));
         }
-    }
-
-    @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
-    /*
-     * Delete all bindings of a user's disk encryption key except the most
-     * recently added one.
-     */
-    @Override
-    public void fixateNewestUserKeyAuth(int userId) {
-
-        try {
-            mVold.fixateNewestUserKeyAuth(userId);
-        } catch (Exception e) {
-            Slog.wtf(TAG, e);
-        }
-    }
-
-    @Override
-    public void unlockUserKey(int userId, int serialNumber, byte[] secret) {
-        boolean isFileEncrypted = StorageManager.isFileEncrypted();
-        Slog.d(TAG, "unlockUserKey: " + userId
-                + " isFileEncrypted: " + isFileEncrypted
-                + " hasSecret: " + (secret != null));
-        enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
-
-        if (isUserKeyUnlocked(userId)) {
-            Slog.d(TAG, "User " + userId + "'s CE storage is already unlocked");
-            return;
-        }
-
-        if (isFileEncrypted) {
-            // When a user has a secure lock screen, a secret is required to
-            // unlock the key, so don't bother trying to unlock it without one.
-            // This prevents misleading error messages from being logged.
-            if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) {
-                Slog.d(TAG, "Not unlocking user " + userId
-                        + "'s CE storage yet because a secret is needed");
-                return;
-            }
-            try {
-                mVold.unlockUserKey(userId, serialNumber, encodeBytes(secret));
-            } catch (Exception e) {
-                Slog.wtf(TAG, e);
-                return;
-            }
-        }
-
         synchronized (mLock) {
             mLocalUnlockedUsers.append(userId);
         }
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index e81bab1..202f4775 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -1831,7 +1831,7 @@
         if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock
                 || mWindowManager.isKeyguardShowingAndNotOccluded()
                 || !mPowerManager.isInteractive())) {
-            Sandman.startDreamWhenDockedIfAppropriate(getContext());
+            mInjector.startDreamWhenDockedIfAppropriate(getContext());
         }
     }
 
@@ -2148,5 +2148,9 @@
         public int getCallingUid() {
             return Binder.getCallingUid();
         }
+
+        public void startDreamWhenDockedIfAppropriate(Context context) {
+            Sandman.startDreamWhenDockedIfAppropriate(context);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3130d2e..34e3eec 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3528,7 +3528,7 @@
             if (subject != null || criticalEventSection != null) {
                 appendtoANRFile(tracesFile.getAbsolutePath(),
                         (subject != null ? "Subject: " + subject + "\n\n" : "")
-                        + criticalEventSection != null ? criticalEventSection : "");
+                        + (criticalEventSection != null ? criticalEventSection : ""));
             }
 
             Pair<Long, Long> offsets = dumpStackTraces(
@@ -6036,6 +6036,7 @@
      * This can be called with or without the global lock held.
      */
     @Override
+    @PackageManager.PermissionResult
     @PermissionMethod
     public int checkPermission(@PermissionName String permission, int pid, int uid) {
         if (permission == null) {
@@ -6048,6 +6049,7 @@
      * Binder IPC calls go through the public entry point.
      * This can be called with or without the global lock held.
      */
+    @PackageManager.PermissionResult
     @PermissionMethod
     int checkCallingPermission(@PermissionName String permission) {
         return checkPermission(permission,
@@ -16401,23 +16403,17 @@
         return mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
     }
 
-    /**
-     * Unlocks the given user.
-     *
-     * @param userId The ID of the user to unlock.
-     * @param token No longer used.  (This parameter cannot be removed because
-     *              this method is marked with UnsupportedAppUsage, so its
-     *              signature might not be safe to change.)
-     * @param secret The secret needed to unlock the user's credential-encrypted
-     *               storage, or null if no secret is needed.
-     * @param listener An optional progress listener.
-     *
-     * @return true if the user was successfully unlocked, otherwise false.
-     */
+    /** @deprecated see the AIDL documentation {@inheritDoc} */
     @Override
-    public boolean unlockUser(int userId, @Nullable byte[] token, @Nullable byte[] secret,
-            @Nullable IProgressListener listener) {
-        return mUserController.unlockUser(userId, secret, listener);
+    @Deprecated
+    public boolean unlockUser(@UserIdInt int userId, @Nullable byte[] token,
+            @Nullable byte[] secret, @Nullable IProgressListener listener) {
+        return mUserController.unlockUser(userId, listener);
+    }
+
+    @Override
+    public boolean unlockUser2(@UserIdInt int userId, @Nullable IProgressListener listener) {
+        return mUserController.unlockUser(userId, listener);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 10e2aae..e4f947d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -2119,7 +2119,7 @@
             return -1;
         }
 
-        boolean success = mInterface.unlockUser(userId, null, null, null);
+        boolean success = mInterface.unlockUser2(userId, null);
         if (success) {
             pw.println("Success: user unlocked");
         } else {
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 4574302..cbf0aae 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -112,6 +112,8 @@
     private static final String ATRACE_COMPACTION_TRACK = "Compaction";
     private static final String ATRACE_FREEZER_TRACK = "Freezer";
 
+    private static final int FREEZE_BINDER_TIMEOUT_MS = 100;
+
     // Defaults for phenotype flags.
     @VisibleForTesting static final Boolean DEFAULT_USE_COMPACTION = false;
     @VisibleForTesting static final Boolean DEFAULT_USE_FREEZER = true;
@@ -929,11 +931,13 @@
      * @param pid the target pid for which binder transactions are to be frozen
      * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
      * binder for the specificed pid.
+     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
+     * before giving up.
      *
      * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
      * @return 0 if success, or -EAGAIN indicating there's pending transaction.
      */
-    private static native int freezeBinder(int pid, boolean freeze);
+    public static native int freezeBinder(int pid, boolean freeze, int timeoutMs);
 
     /**
      * Retrieves binder freeze info about a process.
@@ -1300,7 +1304,7 @@
         long freezeTime = opt.getFreezeUnfreezeTime();
 
         try {
-            freezeBinder(pid, false);
+            freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
         } catch (RuntimeException e) {
             Slog.e(TAG_AM, "Unable to unfreeze binder for " + pid + " " + app.processName
                     + ". Killing it");
@@ -1355,7 +1359,7 @@
             }
             Slog.d(TAG_AM, "quick sync unfreeze " + pid);
             try {
-                freezeBinder(pid, false);
+                freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
             } catch (RuntimeException e) {
                 Slog.e(TAG_AM, "Unable to quick unfreeze binder for " + pid);
                 return;
@@ -1950,7 +1954,7 @@
                 // Freeze binder interface before the process, to flush any
                 // transactions that might be pending.
                 try {
-                    if (freezeBinder(pid, true) != 0) {
+                    if (freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
                         rescheduleFreeze(proc, "outstanding txns");
                         return;
                     }
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index d7b3848..42bfc4c 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -32,6 +32,7 @@
 import static android.os.Process.getTotalMemory;
 import static android.os.Process.killProcessQuiet;
 import static android.os.Process.startWebView;
+import static android.system.OsConstants.*;
 
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LRU;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_NETWORK;
@@ -2711,6 +2712,50 @@
         }
     }
 
+    private static boolean freezePackageCgroup(int packageUID, boolean freeze) {
+        try {
+            Process.freezeCgroupUid(packageUID, freeze);
+        } catch (RuntimeException e) {
+            final String logtxt = freeze ? "freeze" : "unfreeze";
+            Slog.e(TAG, "Unable to " + logtxt + " cgroup uid: " + packageUID + ": " + e);
+            return false;
+        }
+        return true;
+    }
+
+    private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs,
+                                                     int packageUID) {
+        // Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
+        // Since we're going to kill these, we don't need to unfreze them later.
+        // The procs list may not include all processes under the UID cgroup, but unincluded
+        // processes (forks) should not be Binder users.
+        int N = procs.size();
+        for (int i = 0; i < N; i++) {
+            final int uid = procs.get(i).first.uid;
+            final int pid = procs.get(i).first.getPid();
+            int nRetries = 0;
+            // We only freeze the cgroup of the target package, so we do not need to freeze the
+            // Binder interfaces of dependant processes in other UIDs.
+            if (pid > 0 && uid == packageUID) {
+                try {
+                    int rc;
+                    do {
+                        rc = CachedAppOptimizer.freezeBinder(pid, true, 10 /* timeout_ms */);
+                    } while (rc == -EAGAIN && nRetries++ < 1);
+                    if (rc != 0) Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + rc);
+                } catch (RuntimeException e) {
+                    Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + e);
+                }
+            }
+        }
+
+        // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze
+        // despite being added to a new child cgroup. The cgroups of package dependant processes are
+        // not frozen, since it's possible this would freeze processes with no dependency on the
+        // package being killed here.
+        freezePackageCgroup(packageUID, true);
+    }
+
     @GuardedBy({"mService", "mProcLock"})
     boolean killPackageProcessesLSP(String packageName, int appId,
             int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart,
@@ -2763,7 +2808,7 @@
                 boolean shouldAllowRestart = false;
 
                 // If no package is specified, we call all processes under the
-                // give user id.
+                // given user id.
                 if (packageName == null) {
                     if (userId != UserHandle.USER_ALL && app.userId != userId) {
                         continue;
@@ -2806,14 +2851,24 @@
             }
         }
 
+        final int packageUID = UserHandle.getUid(userId, appId);
+        final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID
+                              && appId <= Process.LAST_APPLICATION_UID;
+        if (doFreeze) {
+            freezeBinderAndPackageCgroup(procs, packageUID);
+        }
+
         int N = procs.size();
         for (int i=0; i<N; i++) {
             final Pair<ProcessRecord, Boolean> proc = procs.get(i);
             removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
-                    reasonCode, subReason, reason);
+                    reasonCode, subReason, reason, !doFreeze /* async */);
         }
         killAppZygotesLocked(packageName, appId, userId, false /* force */);
         mService.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_PROCESS_END);
+        if (doFreeze) {
+            freezePackageCgroup(packageUID, false);
+        }
         return N > 0;
     }
 
@@ -2821,12 +2876,19 @@
     boolean removeProcessLocked(ProcessRecord app,
             boolean callerWillRestart, boolean allowRestart, int reasonCode, String reason) {
         return removeProcessLocked(app, callerWillRestart, allowRestart, reasonCode,
-                ApplicationExitInfo.SUBREASON_UNKNOWN, reason);
+                ApplicationExitInfo.SUBREASON_UNKNOWN, reason, true);
     }
 
     @GuardedBy("mService")
     boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart,
             boolean allowRestart, int reasonCode, int subReason, String reason) {
+        return removeProcessLocked(app, callerWillRestart, allowRestart, reasonCode, subReason,
+                reason, true);
+    }
+
+    @GuardedBy("mService")
+    boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart,
+            boolean allowRestart, int reasonCode, int subReason, String reason, boolean async) {
         final String name = app.processName;
         final int uid = app.uid;
         if (DEBUG_PROCESSES) Slog.d(TAG_PROCESSES,
@@ -2863,7 +2925,7 @@
                     needRestart = true;
                 }
             }
-            app.killLocked(reason, reasonCode, subReason, true);
+            app.killLocked(reason, reasonCode, subReason, true, async);
             mService.handleAppDiedLocked(app, pid, willRestart, allowRestart,
                     false /* fromBinderDied */);
             if (willRestart) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 482e6a7..cc9071a 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -1056,18 +1056,30 @@
 
     @GuardedBy("mService")
     void killLocked(String reason, @Reason int reasonCode, boolean noisy) {
-        killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy);
+        killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy, true);
     }
 
     @GuardedBy("mService")
     void killLocked(String reason, @Reason int reasonCode, @SubReason int subReason,
             boolean noisy) {
-        killLocked(reason, reason, reasonCode, subReason, noisy);
+        killLocked(reason, reason, reasonCode, subReason, noisy, true);
     }
 
     @GuardedBy("mService")
     void killLocked(String reason, String description, @Reason int reasonCode,
             @SubReason int subReason, boolean noisy) {
+        killLocked(reason, description, reasonCode, subReason, noisy, true);
+    }
+
+    @GuardedBy("mService")
+    void killLocked(String reason, @Reason int reasonCode, @SubReason int subReason,
+            boolean noisy, boolean asyncKPG) {
+        killLocked(reason, reason, reasonCode, subReason, noisy, asyncKPG);
+    }
+
+    @GuardedBy("mService")
+    void killLocked(String reason, String description, @Reason int reasonCode,
+            @SubReason int subReason, boolean noisy, boolean asyncKPG) {
         if (!mKilledByAm) {
             Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");
             if (reasonCode == ApplicationExitInfo.REASON_ANR
@@ -1084,7 +1096,8 @@
                 EventLog.writeEvent(EventLogTags.AM_KILL,
                         userId, mPid, processName, mState.getSetAdj(), reason);
                 Process.killProcessQuiet(mPid);
-                ProcessList.killProcessGroup(uid, mPid);
+                if (asyncKPG) ProcessList.killProcessGroup(uid, mPid);
+                else Process.killProcessGroup(uid, mPid);
             } else {
                 mPendingStart = false;
             }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 470de8c..f16347f 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -90,6 +90,7 @@
         DeviceConfig.NAMESPACE_NETD_NATIVE,
         DeviceConfig.NAMESPACE_NNAPI_NATIVE,
         DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
+        DeviceConfig.NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE,
         DeviceConfig.NAMESPACE_RUNTIME_NATIVE,
         DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
         DeviceConfig.NAMESPACE_STATSD_NATIVE,
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 82fb1e8..226c638 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -654,7 +654,7 @@
         EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKING, userId);
         logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER,
                 USER_LIFECYCLE_EVENT_STATE_BEGIN);
-        // Only keep marching forward if user is actually unlocked
+        // If the user key hasn't been unlocked yet, we cannot proceed.
         if (!StorageManager.isUserKeyUnlocked(userId)) return false;
         synchronized (mLock) {
             // Do not proceed if unexpected state or a stale user
@@ -1776,28 +1776,19 @@
         }
     }
 
-    boolean unlockUser(final @UserIdInt int userId, byte[] secret, IProgressListener listener) {
+    boolean unlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) {
         checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "unlockUser");
         EventLog.writeEvent(EventLogTags.UC_UNLOCK_USER, userId);
         final long binderToken = Binder.clearCallingIdentity();
         try {
-            return unlockUserCleared(userId, secret, listener);
+            return maybeUnlockUser(userId, listener);
         } finally {
             Binder.restoreCallingIdentity(binderToken);
         }
     }
 
-    /**
-     * Attempt to unlock user without a secret. This typically succeeds when the
-     * device doesn't have credential-encrypted storage, or when the
-     * credential-encrypted storage isn't tied to a user-provided PIN or
-     * pattern.
-     */
-    private boolean maybeUnlockUser(final @UserIdInt int userId) {
-        return unlockUserCleared(userId, null, null);
-    }
-
-    private static void notifyFinished(@UserIdInt int userId, IProgressListener listener) {
+    private static void notifyFinished(@UserIdInt int userId,
+            @Nullable IProgressListener listener) {
         if (listener == null) return;
         try {
             listener.onFinished(userId, null);
@@ -1805,8 +1796,18 @@
         }
     }
 
-    private boolean unlockUserCleared(final @UserIdInt int userId, byte[] secret,
-            IProgressListener listener) {
+    private boolean maybeUnlockUser(@UserIdInt int userId) {
+        return maybeUnlockUser(userId, null);
+    }
+
+    /**
+     * Tries to unlock the given user.
+     * <p>
+     * This will succeed only if the user's CE storage key is already unlocked or if the user
+     * doesn't have a lockscreen credential set.
+     */
+    private boolean maybeUnlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) {
+
         // Delay user unlocking for headless system user mode until the system boot
         // completes. When the system boot completes, the {@link #onBootCompleted()}
         // method unlocks all started users for headless system user mode. This is done
@@ -1825,14 +1826,8 @@
 
         UserState uss;
         if (!StorageManager.isUserKeyUnlocked(userId)) {
-            final UserInfo userInfo = getUserInfo(userId);
-            final IStorageManager storageManager = mInjector.getStorageManager();
-            try {
-                // We always want to unlock user storage, even user is not started yet
-                storageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
-            } catch (RemoteException | RuntimeException e) {
-                Slogf.w(TAG, "Failed to unlock: " + e.getMessage());
-            }
+            // We always want to try to unlock the user key, even if the user is not started yet.
+            mLockPatternUtils.unlockUserKeyIfUnsecured(userId);
         }
         synchronized (mLock) {
             // Register the given listener to watch for unlock progress
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 85ec83c..271bce9 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
@@ -588,9 +588,8 @@
                 }
                 try {
                     final SensorProps[] props = face.getSensorProps();
-                    final FaceProvider provider = new FaceProvider(getContext(),
-                            mBiometricStateCallback, props, instance, mLockoutResetDispatcher,
-                            BiometricContext.getInstance(getContext()));
+                    final FaceProvider provider = new FaceProvider(getContext(), props, instance,
+                            mLockoutResetDispatcher, BiometricContext.getInstance(getContext()));
                     providers.add(provider);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
@@ -601,14 +600,14 @@
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FaceSensorPropertiesInternal> hidlSensors) {
             mRegistry.registerAll(() -> {
                 final List<ServiceProvider> providers = new ArrayList<>();
                 for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
                     providers.add(
-                            Face10.newInstance(getContext(), mBiometricStateCallback,
-                                    hidlSensor, mLockoutResetDispatcher));
+                            Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher));
                 }
                 providers.addAll(getAidlProviders());
                 return providers;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
index 43b5896..73c272f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.face.aidl;
 
+import static android.Manifest.permission.TEST_BIOMETRIC;
+
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.ITestSession;
@@ -31,8 +33,8 @@
 import android.util.Slog;
 
 import com.android.server.biometrics.HardwareAuthTokenUtils;
+import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index 2d7b2e6..6bff179 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,10 +52,8 @@
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.PerformanceTracker;
@@ -82,7 +80,6 @@
     private boolean mTestHalEnabled;
 
     @NonNull private final Context mContext;
-    @NonNull private final BiometricStateCallback mBiometricStateCallback;
     @NonNull private final String mHalInstanceName;
     @NonNull @VisibleForTesting
     final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports
@@ -124,14 +121,11 @@
         }
     }
 
-    public FaceProvider(@NonNull Context context,
-            @NonNull BiometricStateCallback biometricStateCallback,
-            @NonNull SensorProps[] props,
+    public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props,
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull BiometricContext biometricContext) {
         mContext = context;
-        mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
         mSensors = new SparseArray<>();
         mHandler = new Handler(Looper.getMainLooper());
@@ -370,19 +364,8 @@
                     mBiometricContext, maxTemplatesPerUser, debugConsent);
             scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
                 @Override
-                public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
-                    mBiometricStateCallback.onClientStarted(clientMonitor);
-                }
-
-                @Override
-                public void onBiometricAction(int action) {
-                    mBiometricStateCallback.onBiometricAction(action);
-                }
-
-                @Override
                 public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
                         boolean success) {
-                    mBiometricStateCallback.onClientFinished(clientMonitor, success);
                     if (success) {
                         scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
                         scheduleInvalidationRequest(sensorId, userId);
@@ -412,7 +395,7 @@
                     token, id, callback, userId, opPackageName, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric);
-            scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            scheduleForSensor(sensorId, client);
         });
 
         return id;
@@ -439,7 +422,7 @@
                     mBiometricContext, isStrongBiometric,
                     mUsageStats, mSensors.get(sensorId).getLockoutCache(),
                     allowBackgroundAuthentication, isKeyguardBypassEnabled);
-            scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            scheduleForSensor(sensorId, client);
         });
     }
 
@@ -494,7 +477,7 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext,
                     mSensors.get(sensorId).getAuthenticatorIds());
-            scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            scheduleForSensor(sensorId, client);
         });
     }
 
@@ -582,8 +565,7 @@
             if (favorHalEnrollments) {
                 client.setFavorHalEnrollments();
             }
-            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback,
-                    mBiometricStateCallback));
+            scheduleForSensor(sensorId, client, callback);
         });
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index f1c6512..800d4b8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -57,7 +57,6 @@
 import com.android.server.biometrics.sensors.AuthenticationConsumer;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.EnumerateConsumer;
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.Interruptable;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
index 698d064..14af216 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.face.hidl;
 
+import static android.Manifest.permission.TEST_BIOMETRIC;
+
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.ITestSession;
@@ -28,8 +30,8 @@
 import android.os.RemoteException;
 import android.util.Slog;
 
+import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 
@@ -51,7 +53,6 @@
     @NonNull private final Set<Integer> mEnrollmentIds;
     @NonNull private final Random mRandom;
 
-
     private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() {
         @Override
         public void onEnrollResult(Face face, int remaining) {
@@ -115,8 +116,7 @@
     };
 
     BiometricTestSessionImpl(@NonNull Context context, int sensorId,
-            @NonNull ITestSessionCallback callback,
-            @NonNull Face10 face10,
+            @NonNull ITestSessionCallback callback, @NonNull Face10 face10,
             @NonNull Face10.HalResultController halResultController) {
         mContext = context;
         mSensorId = sensorId;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index 0e0ee19..c0a119f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -62,10 +62,8 @@
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.EnumerateConsumer;
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -112,7 +110,6 @@
     private boolean mTestHalEnabled;
 
     @NonNull private final FaceSensorPropertiesInternal mSensorProperties;
-    @NonNull private final BiometricStateCallback mBiometricStateCallback;
     @NonNull private final Context mContext;
     @NonNull private final BiometricScheduler mScheduler;
     @NonNull private final Handler mHandler;
@@ -339,7 +336,6 @@
 
     @VisibleForTesting
     Face10(@NonNull Context context,
-            @NonNull BiometricStateCallback biometricStateCallback,
             @NonNull FaceSensorPropertiesInternal sensorProps,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull Handler handler,
@@ -347,7 +343,6 @@
             @NonNull BiometricContext biometricContext) {
         mSensorProperties = sensorProps;
         mContext = context;
-        mBiometricStateCallback = biometricStateCallback;
         mSensorId = sensorProps.sensorId;
         mScheduler = scheduler;
         mHandler = handler;
@@ -371,12 +366,11 @@
     }
 
     public static Face10 newInstance(@NonNull Context context,
-            @NonNull BiometricStateCallback biometricStateCallback,
             @NonNull FaceSensorPropertiesInternal sensorProps,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
         final Handler handler = new Handler(Looper.getMainLooper());
-        return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher,
-                handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+        return new Face10(context, sensorProps, lockoutResetDispatcher, handler,
+                new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
                         null /* gestureAvailabilityTracker */),
                 BiometricContext.getInstance(context));
     }
@@ -621,19 +615,8 @@
 
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
                 @Override
-                public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
-                    mBiometricStateCallback.onClientStarted(clientMonitor);
-                }
-
-                @Override
-                public void onBiometricAction(int action) {
-                    mBiometricStateCallback.onBiometricAction(action);
-                }
-
-                @Override
                 public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
                         boolean success) {
-                    mBiometricStateCallback.onClientFinished(clientMonitor, success);
                     if (success) {
                         // Update authenticatorIds
                         scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId());
@@ -678,7 +661,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric, mLockoutTracker,
                     mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled);
-            mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
+            mScheduler.scheduleClientMonitor(client);
         });
     }
 
@@ -713,7 +696,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, mAuthenticatorIds);
-            mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
+            mScheduler.scheduleClientMonitor(client);
         });
     }
 
@@ -731,7 +714,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, mAuthenticatorIds);
-            mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
+            mScheduler.scheduleClientMonitor(client);
         });
     }
 
@@ -823,15 +806,14 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, enrolledList,
                     FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
-            mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback,
-                    mBiometricStateCallback));
+            mScheduler.scheduleClientMonitor(client, callback);
         });
     }
 
     @Override
     public void scheduleInternalCleanup(int sensorId, int userId,
             @Nullable ClientMonitorCallback callback) {
-        scheduleInternalCleanup(userId, mBiometricStateCallback);
+        scheduleInternalCleanup(userId, callback);
     }
 
     @Override
@@ -1029,7 +1011,7 @@
     @Override
     public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName) {
-        return new BiometricTestSessionImpl(mContext, mSensorId, callback,
-                this, mHalResultController);
+        return new BiometricTestSessionImpl(mContext, mSensorId, callback, this,
+                mHalResultController);
     }
 }
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index b3aee22..7b60421 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -593,10 +593,14 @@
         }
 
         pw.println();
+        pw.println("  mAmbientBrightnessThresholds=");
         mAmbientBrightnessThresholds.dump(pw);
+        pw.println("  mScreenBrightnessThresholds=");
         mScreenBrightnessThresholds.dump(pw);
+        pw.println("  mScreenBrightnessThresholdsIdle=");
         mScreenBrightnessThresholdsIdle.dump(pw);
-        mScreenBrightnessThresholdsIdle.dump(pw);
+        pw.println("  mAmbientBrightnessThresholdsIdle=");
+        mAmbientBrightnessThresholdsIdle.dump(pw);
     }
 
     private String configStateToString(int state) {
@@ -861,6 +865,7 @@
                 Slog.d(TAG, "updateAmbientLux: "
                         + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
                         + "mBrighteningLuxThreshold=" + mAmbientBrighteningThreshold + ", "
+                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
                         + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
                         + "mAmbientLux=" + mAmbientLux);
             }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4165186..81219ba 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -27,6 +27,7 @@
 import android.os.PowerManager;
 import android.text.TextUtils;
 import android.util.MathUtils;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.Spline;
 import android.view.DisplayAddress;
@@ -51,7 +52,7 @@
 import com.android.server.display.config.SensorDetails;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.config.ThermalThrottling;
-import com.android.server.display.config.Thresholds;
+import com.android.server.display.config.ThresholdPoint;
 import com.android.server.display.config.XmlParser;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -188,42 +189,153 @@
  *      <ambientLightHorizonLong>10001</ambientLightHorizonLong>
  *      <ambientLightHorizonShort>2001</ambientLightHorizonShort>
  *
- *      <displayBrightnessChangeThresholds> // Thresholds for screen changes
- *        <brighteningThresholds>     // Thresholds for active mode brightness changes.
- *          <minimum>0.001</minimum>  // Minimum change needed in screen brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.002</minimum>  // Minimum change needed in screen brightness to darken.
- *        </darkeningThresholds>
- *      </displayBrightnessChangeThresholds>
- *
- *      <ambientBrightnessChangeThresholds> // Thresholds for lux changes
- *        <brighteningThresholds>     // Thresholds for active mode brightness changes.
- *          <minimum>0.003</minimum>  // Minimum change needed in ambient brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.004</minimum>  // Minimum change needed in ambient brightness to darken.
- *        </darkeningThresholds>
- *      </ambientBrightnessChangeThresholds>
- *
- *      <displayBrightnessChangeThresholdsIdle> // Thresholds for screen changes in idle mode
- *        <brighteningThresholds>     // Thresholds for idle mode brightness changes.
- *          <minimum>0.001</minimum>  // Minimum change needed in screen brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.002</minimum>  // Minimum change needed in screen brightness to darken.
- *        </darkeningThresholds>
- *      </displayBrightnessChangeThresholdsIdle>
- *
- *      <ambientBrightnessChangeThresholdsIdle> // Thresholds for lux changes in idle mode
- *        <brighteningThresholds>     // Thresholds for idle mode brightness changes.
- *          <minimum>0.003</minimum>  // Minimum change needed in ambient brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.004</minimum>  // Minimum change needed in ambient brightness to darken.
- *        </darkeningThresholds>
- *      </ambientBrightnessChangeThresholdsIdle>
- *
+ *     <ambientBrightnessChangeThresholds>  // Thresholds for lux changes
+ *         <brighteningThresholds>
+ *             // Minimum change needed in ambient brightness to brighten screen.
+ *             <minimum>10</minimum>
+ *             // Percentage increase of lux needed to increase the screen brightness at a lux range
+ *             // above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>13</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>100</threshold><percentage>14</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>200</threshold><percentage>15</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in ambient brightness to darken screen.
+ *             <minimum>30</minimum>
+ *             // Percentage increase of lux needed to decrease the screen brightness at a lux range
+ *             // above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>15</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>300</threshold><percentage>16</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>400</threshold><percentage>17</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </ambientBrightnessChangeThresholds>
+ *     <displayBrightnessChangeThresholds>   // Thresholds for screen brightness changes
+ *         <brighteningThresholds>
+ *             // Minimum change needed in screen brightness to brighten screen.
+ *             <minimum>0.1</minimum>
+ *             // Percentage increase of screen brightness needed to increase the screen brightness
+ *             // at a lux range above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold>
+ *                     <percentage>9</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.10</threshold>
+ *                     <percentage>10</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.20</threshold>
+ *                     <percentage>11</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in screen brightness to darken screen.
+ *             <minimum>0.3</minimum>
+ *             // Percentage increase of screen brightness needed to decrease the screen brightness
+ *             // at a lux range above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>11</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.11</threshold><percentage>12</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.21</threshold><percentage>13</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </displayBrightnessChangeThresholds>
+ *     <ambientBrightnessChangeThresholdsIdle>   // Thresholds for lux changes in idle mode
+ *         <brighteningThresholds>
+ *             // Minimum change needed in ambient brightness to brighten screen in idle mode
+ *             <minimum>20</minimum>
+ *             // Percentage increase of lux needed to increase the screen brightness at a lux range
+ *             // above the specified threshold whilst in idle mode.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>21</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>500</threshold><percentage>22</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>600</threshold><percentage>23</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in ambient brightness to darken screen in idle mode
+ *             <minimum>40</minimum>
+ *             // Percentage increase of lux needed to decrease the screen brightness at a lux range
+ *             // above the specified threshold whilst in idle mode.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>23</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>700</threshold><percentage>24</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>800</threshold><percentage>25</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </ambientBrightnessChangeThresholdsIdle>
+ *     <displayBrightnessChangeThresholdsIdle>    // Thresholds for idle screen brightness changes
+ *         <brighteningThresholds>
+ *             // Minimum change needed in screen brightness to brighten screen in idle mode
+ *             <minimum>0.2</minimum>
+ *             // Percentage increase of screen brightness needed to increase the screen brightness
+ *             // at a lux range above the specified threshold whilst in idle mode
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>17</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.12</threshold><percentage>18</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.22</threshold><percentage>19</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in screen brightness to darken screen in idle mode
+ *             <minimum>0.4</minimum>
+ *             // Percentage increase of screen brightness needed to decrease the screen brightness
+ *             // at a lux range above the specified threshold whilst in idle mode
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>19</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.13</threshold><percentage>20</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.23</threshold><percentage>21</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </displayBrightnessChangeThresholdsIdle>
  *    </displayConfiguration>
  *  }
  *  </pre>
@@ -247,6 +359,13 @@
     private static final String NO_SUFFIX_FORMAT = "%d";
     private static final long STABLE_FLAG = 1L << 62;
 
+    private static final float[] DEFAULT_AMBIENT_THRESHOLD_LEVELS = new float[]{0f};
+    private static final float[] DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS = new float[]{100f};
+    private static final float[] DEFAULT_AMBIENT_DARKENING_THRESHOLDS = new float[]{200f};
+    private static final float[] DEFAULT_SCREEN_THRESHOLD_LEVELS = new float[]{0f};
+    private static final float[] DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS = new float[]{100f};
+    private static final float[] DEFAULT_SCREEN_DARKENING_THRESHOLDS = new float[]{200f};
+
     private static final int INTERPOLATION_DEFAULT = 0;
     private static final int INTERPOLATION_LINEAR = 1;
 
@@ -344,6 +463,31 @@
     private float mAmbientLuxBrighteningMinThresholdIdle = 0.0f;
     private float mAmbientLuxDarkeningMinThreshold = 0.0f;
     private float mAmbientLuxDarkeningMinThresholdIdle = 0.0f;
+
+    // Screen brightness thresholds levels & percentages
+    private float[] mScreenBrighteningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenBrighteningPercentages = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS;
+    private float[] mScreenDarkeningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenDarkeningPercentages = DEFAULT_SCREEN_DARKENING_THRESHOLDS;
+
+    // Screen brightness thresholds levels & percentages for idle mode
+    private float[] mScreenBrighteningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenBrighteningPercentagesIdle = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS;
+    private float[] mScreenDarkeningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenDarkeningPercentagesIdle = DEFAULT_SCREEN_DARKENING_THRESHOLDS;
+
+    // Ambient brightness thresholds levels & percentages
+    private float[] mAmbientBrighteningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientBrighteningPercentages = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS;
+    private float[] mAmbientDarkeningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientDarkeningPercentages = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+
+    // Ambient brightness thresholds levels & percentages for idle mode
+    private float[] mAmbientBrighteningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientBrighteningPercentagesIdle = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS;
+    private float[] mAmbientDarkeningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientDarkeningPercentagesIdle = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+
     private Spline mBrightnessToBacklightSpline;
     private Spline mBacklightToBrightnessSpline;
     private Spline mBacklightToNitsSpline;
@@ -684,7 +828,7 @@
     /**
      * The minimum value for the ambient lux increase for a screen brightness change to actually
      * occur.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxBrighteningMinThreshold() {
         return mAmbientLuxBrighteningMinThreshold;
@@ -693,7 +837,7 @@
     /**
      * The minimum value for the ambient lux decrease for a screen brightness change to actually
      * occur.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxDarkeningMinThreshold() {
         return mAmbientLuxDarkeningMinThreshold;
@@ -702,7 +846,7 @@
     /**
      * The minimum value for the ambient lux increase for a screen brightness change to actually
      * occur while in idle screen brightness mode.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxBrighteningMinThresholdIdle() {
         return mAmbientLuxBrighteningMinThresholdIdle;
@@ -711,12 +855,262 @@
     /**
      * The minimum value for the ambient lux decrease for a screen brightness change to actually
      * occur while in idle screen brightness mode.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxDarkeningMinThresholdIdle() {
         return mAmbientLuxDarkeningMinThresholdIdle;
     }
 
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenBrighteningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenBrighteningPercentages[n]
+     * level[MAX] <= value             = mScreenBrighteningPercentages[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenBrighteningPercentages applies
+     */
+    public float[] getScreenBrighteningLevels() {
+        return mScreenBrighteningLevels;
+    }
+
+    /**
+     * The array that describes the screen brightening threshold percentage change at each screen
+     * brightness level described in mScreenBrighteningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness increase required in order for the
+     * screen brightness to change
+     */
+    public float[] getScreenBrighteningPercentages() {
+        return mScreenBrighteningPercentages;
+    }
+
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenDarkeningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenDarkeningPercentages[n]
+     * level[MAX] <= value             = mScreenDarkeningPercentages[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenDarkeningPercentages applies
+     */
+    public float[] getScreenDarkeningLevels() {
+        return mScreenDarkeningLevels;
+    }
+
+    /**
+     * The array that describes the screen darkening threshold percentage change at each screen
+     * brightness level described in mScreenDarkeningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness decrease required in order for the
+     * screen brightness to change
+     */
+    public float[] getScreenDarkeningPercentages() {
+        return mScreenDarkeningPercentages;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold
+     * percentage applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientBrighteningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientBrighteningPercentages[n]
+     * level[MAX] <= value             = mAmbientBrighteningPercentages[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientBrighteningPercentages applies
+     */
+    public float[] getAmbientBrighteningLevels() {
+        return mAmbientBrighteningLevels;
+    }
+
+    /**
+     * The array that describes the ambient brightening threshold percentage change at each ambient
+     * brightness level described in mAmbientBrighteningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness increase required in order for the
+     * screen brightness to change
+     */
+    public float[] getAmbientBrighteningPercentages() {
+        return mAmbientBrighteningPercentages;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold percentage
+     * applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientDarkeningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientDarkeningPercentages[n]
+     * level[MAX] <= value             = mAmbientDarkeningPercentages[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientDarkeningPercentages applies
+     */
+    public float[] getAmbientDarkeningLevels() {
+        return mAmbientDarkeningLevels;
+    }
+
+    /**
+     * The array that describes the ambient darkening threshold percentage change at each ambient
+     * brightness level described in mAmbientDarkeningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness decrease required in order for the
+     * screen brightness to change
+     */
+    public float[] getAmbientDarkeningPercentages() {
+        return mAmbientDarkeningPercentages;
+    }
+
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenBrighteningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenBrighteningPercentagesIdle[n]
+     * level[MAX] <= value             = mScreenBrighteningPercentagesIdle[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenBrighteningPercentagesIdle applies
+     */
+    public float[] getScreenBrighteningLevelsIdle() {
+        return mScreenBrighteningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the screen brightening threshold percentage change at each screen
+     * brightness level described in mScreenBrighteningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of brightness increase required in order for the
+     * screen brightness to change while in idle mode.
+     */
+    public float[] getScreenBrighteningPercentagesIdle() {
+        return mScreenBrighteningPercentagesIdle;
+    }
+
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenDarkeningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenDarkeningPercentagesIdle[n]
+     * level[MAX] <= value             = mScreenDarkeningPercentagesIdle[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenDarkeningPercentagesIdle applies
+     */
+    public float[] getScreenDarkeningLevelsIdle() {
+        return mScreenDarkeningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the screen darkening threshold percentage change at each screen
+     * brightness level described in mScreenDarkeningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of brightness decrease required in order for the
+     * screen brightness to change while in idle mode.
+     */
+    public float[] getScreenDarkeningPercentagesIdle() {
+        return mScreenDarkeningPercentagesIdle;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientBrighteningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientBrighteningPercentagesIdle[n]
+     * level[MAX] <= value             = mAmbientBrighteningPercentagesIdle[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientBrighteningPercentagesIdle applies
+     */
+    public float[] getAmbientBrighteningLevelsIdle() {
+        return mAmbientBrighteningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the ambient brightness threshold percentage change whilst in
+     * idle screen brightness mode at each ambient brightness level described in
+     * mAmbientBrighteningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of ambient brightness increase required in order
+     * for the screen brightness to change
+     */
+    public float[] getAmbientBrighteningPercentagesIdle() {
+        return mAmbientBrighteningPercentagesIdle;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientDarkeningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientDarkeningPercentagesIdle[n]
+     * level[MAX] <= value             = mAmbientDarkeningPercentagesIdle[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientDarkeningPercentagesIdle applies
+     */
+    public float[] getAmbientDarkeningLevelsIdle() {
+        return mAmbientDarkeningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the ambient brightness threshold percentage change whilst in
+     * idle screen brightness mode at each ambient brightness level described in
+     * mAmbientDarkeningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of ambient brightness decrease required in order
+     * for the screen brightness to change
+     */
+    public float[] getAmbientDarkeningPercentagesIdle() {
+        return mAmbientDarkeningPercentagesIdle;
+    }
+
     SensorData getAmbientLightSensor() {
         return mAmbientLightSensor;
     }
@@ -812,14 +1206,17 @@
                 + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
                 + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData
                 + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData
+                + "\n"
                 + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease
                 + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease
                 + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease
                 + ", mBrightnessRampSlowIncrease=" + mBrightnessRampSlowIncrease
                 + ", mBrightnessRampDecreaseMaxMillis=" + mBrightnessRampDecreaseMaxMillis
                 + ", mBrightnessRampIncreaseMaxMillis=" + mBrightnessRampIncreaseMaxMillis
+                + "\n"
                 + ", mAmbientHorizonLong=" + mAmbientHorizonLong
                 + ", mAmbientHorizonShort=" + mAmbientHorizonShort
+                + "\n"
                 + ", mScreenDarkeningMinThreshold=" + mScreenDarkeningMinThreshold
                 + ", mScreenDarkeningMinThresholdIdle=" + mScreenDarkeningMinThresholdIdle
                 + ", mScreenBrighteningMinThreshold=" + mScreenBrighteningMinThreshold
@@ -829,6 +1226,41 @@
                 + ", mAmbientLuxBrighteningMinThreshold=" + mAmbientLuxBrighteningMinThreshold
                 + ", mAmbientLuxBrighteningMinThresholdIdle="
                 + mAmbientLuxBrighteningMinThresholdIdle
+                + "\n"
+                + ", mScreenBrighteningLevels=" + Arrays.toString(
+                mScreenBrighteningLevels)
+                + ", mScreenBrighteningPercentages=" + Arrays.toString(
+                mScreenBrighteningPercentages)
+                + ", mScreenDarkeningLevels=" + Arrays.toString(
+                mScreenDarkeningLevels)
+                + ", mScreenDarkeningPercentages=" + Arrays.toString(
+                mScreenDarkeningPercentages)
+                + ", mAmbientBrighteningLevels=" + Arrays.toString(
+                mAmbientBrighteningLevels)
+                + ", mAmbientBrighteningPercentages=" + Arrays.toString(
+                mAmbientBrighteningPercentages)
+                + ", mAmbientDarkeningLevels=" + Arrays.toString(
+                mAmbientDarkeningLevels)
+                + ", mAmbientDarkeningPercentages=" + Arrays.toString(
+                mAmbientDarkeningPercentages)
+                + "\n"
+                + ", mAmbientBrighteningLevelsIdle=" + Arrays.toString(
+                mAmbientBrighteningLevelsIdle)
+                + ", mAmbientBrighteningPercentagesIdle=" + Arrays.toString(
+                mAmbientBrighteningPercentagesIdle)
+                + ", mAmbientDarkeningLevelsIdle=" + Arrays.toString(
+                mAmbientDarkeningLevelsIdle)
+                + ", mAmbientDarkeningPercentagesIdle=" + Arrays.toString(
+                mAmbientDarkeningPercentagesIdle)
+                + ", mScreenBrighteningLevelsIdle=" + Arrays.toString(
+                mScreenBrighteningLevelsIdle)
+                + ", mScreenBrighteningPercentagesIdle=" + Arrays.toString(
+                mScreenBrighteningPercentagesIdle)
+                + ", mScreenDarkeningLevelsIdle=" + Arrays.toString(
+                mScreenDarkeningLevelsIdle)
+                + ", mScreenDarkeningPercentagesIdle=" + Arrays.toString(
+                mScreenDarkeningPercentagesIdle)
+                + "\n"
                 + ", mAmbientLightSensor=" + mAmbientLightSensor
                 + ", mProximitySensor=" + mProximitySensor
                 + ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray())
@@ -914,6 +1346,7 @@
         loadBrightnessMapFromConfigXml();
         loadBrightnessRampsFromConfigXml();
         loadAmbientLightSensorFromConfigXml();
+        loadBrightnessChangeThresholdsFromXml();
         setProxSensorUnspecified();
         loadAutoBrightnessConfigsFromConfigXml();
         mLoadedFrom = "<config.xml>";
@@ -1454,91 +1887,286 @@
         }
     }
 
+    private void loadBrightnessChangeThresholdsFromXml() {
+        loadBrightnessChangeThresholds(/* config= */ null);
+    }
+
     private void loadBrightnessChangeThresholds(DisplayConfiguration config) {
-        Thresholds displayBrightnessThresholds = config.getDisplayBrightnessChangeThresholds();
-        Thresholds ambientBrightnessThresholds = config.getAmbientBrightnessChangeThresholds();
-        Thresholds displayBrightnessThresholdsIdle =
-                config.getDisplayBrightnessChangeThresholdsIdle();
-        Thresholds ambientBrightnessThresholdsIdle =
-                config.getAmbientBrightnessChangeThresholdsIdle();
-
-        loadDisplayBrightnessThresholds(displayBrightnessThresholds);
-        loadAmbientBrightnessThresholds(ambientBrightnessThresholds);
-        loadIdleDisplayBrightnessThresholds(displayBrightnessThresholdsIdle);
-        loadIdleAmbientBrightnessThresholds(ambientBrightnessThresholdsIdle);
+        loadDisplayBrightnessThresholds(config);
+        loadAmbientBrightnessThresholds(config);
+        loadDisplayBrightnessThresholdsIdle(config);
+        loadAmbientBrightnessThresholdsIdle(config);
     }
 
-    private void loadDisplayBrightnessThresholds(Thresholds displayBrightnessThresholds) {
-        if (displayBrightnessThresholds != null) {
-            BrightnessThresholds brighteningScreen =
-                    displayBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningScreen =
-                    displayBrightnessThresholds.getDarkeningThresholds();
+    private void loadDisplayBrightnessThresholds(DisplayConfiguration config) {
+        BrightnessThresholds brighteningScreen = null;
+        BrightnessThresholds darkeningScreen = null;
+        if (config != null && config.getDisplayBrightnessChangeThresholds() != null) {
+            brighteningScreen =
+                    config.getDisplayBrightnessChangeThresholds().getBrighteningThresholds();
+            darkeningScreen =
+                    config.getDisplayBrightnessChangeThresholds().getDarkeningThresholds();
 
-            if (brighteningScreen != null && brighteningScreen.getMinimum() != null) {
-                mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue();
-            }
-            if (darkeningScreen != null && darkeningScreen.getMinimum() != null) {
-                mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue();
-            }
+        }
+
+        // Screen bright/darkening threshold levels for active mode
+        Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningScreen,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenBrighteningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+
+        mScreenBrighteningLevels = screenBrighteningPair.first;
+        mScreenBrighteningPercentages = screenBrighteningPair.second;
+
+        Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningScreen,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenDarkeningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+        mScreenDarkeningLevels = screenDarkeningPair.first;
+        mScreenDarkeningPercentages = screenDarkeningPair.second;
+
+        // Screen bright/darkening threshold minimums for active mode
+        if (brighteningScreen != null && brighteningScreen.getMinimum() != null) {
+            mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue();
+        }
+        if (darkeningScreen != null && darkeningScreen.getMinimum() != null) {
+            mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue();
         }
     }
 
-    private void loadAmbientBrightnessThresholds(Thresholds ambientBrightnessThresholds) {
-        if (ambientBrightnessThresholds != null) {
-            BrightnessThresholds brighteningAmbientLux =
-                    ambientBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningAmbientLux =
-                    ambientBrightnessThresholds.getDarkeningThresholds();
+    private void loadAmbientBrightnessThresholds(DisplayConfiguration config) {
+        // Ambient Brightness Threshold Levels
+        BrightnessThresholds brighteningAmbientLux = null;
+        BrightnessThresholds darkeningAmbientLux = null;
+        if (config != null && config.getAmbientBrightnessChangeThresholds() != null) {
+            brighteningAmbientLux =
+                    config.getAmbientBrightnessChangeThresholds().getBrighteningThresholds();
+            darkeningAmbientLux =
+                    config.getAmbientBrightnessChangeThresholds().getDarkeningThresholds();
+        }
 
-            final BigDecimal ambientBrighteningThreshold = brighteningAmbientLux.getMinimum();
-            final BigDecimal ambientDarkeningThreshold = darkeningAmbientLux.getMinimum();
+        // Ambient bright/darkening threshold levels for active mode
+        Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningAmbientLux,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientBrighteningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS);
+        mAmbientBrighteningLevels = ambientBrighteningPair.first;
+        mAmbientBrighteningPercentages = ambientBrighteningPair.second;
 
-            if (ambientBrighteningThreshold != null) {
-                mAmbientLuxBrighteningMinThreshold = ambientBrighteningThreshold.floatValue();
-            }
-            if (ambientDarkeningThreshold != null) {
-                mAmbientLuxDarkeningMinThreshold = ambientDarkeningThreshold.floatValue();
-            }
+        Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningAmbientLux,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientDarkeningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS);
+        mAmbientDarkeningLevels = ambientDarkeningPair.first;
+        mAmbientDarkeningPercentages = ambientDarkeningPair.second;
+
+        // Ambient bright/darkening threshold minimums for active/idle mode
+        if (brighteningAmbientLux != null && brighteningAmbientLux.getMinimum() != null) {
+            mAmbientLuxBrighteningMinThreshold =
+                    brighteningAmbientLux.getMinimum().floatValue();
+        }
+
+        if (darkeningAmbientLux != null && darkeningAmbientLux.getMinimum() != null) {
+            mAmbientLuxDarkeningMinThreshold = darkeningAmbientLux.getMinimum().floatValue();
         }
     }
 
-    private void loadIdleDisplayBrightnessThresholds(Thresholds idleDisplayBrightnessThresholds) {
-        if (idleDisplayBrightnessThresholds != null) {
-            BrightnessThresholds brighteningScreenIdle =
-                    idleDisplayBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningScreenIdle =
-                    idleDisplayBrightnessThresholds.getDarkeningThresholds();
+    private void loadDisplayBrightnessThresholdsIdle(DisplayConfiguration config) {
+        BrightnessThresholds brighteningScreenIdle = null;
+        BrightnessThresholds darkeningScreenIdle = null;
+        if (config != null && config.getDisplayBrightnessChangeThresholdsIdle() != null) {
+            brighteningScreenIdle =
+                    config.getDisplayBrightnessChangeThresholdsIdle().getBrighteningThresholds();
+            darkeningScreenIdle =
+                    config.getDisplayBrightnessChangeThresholdsIdle().getDarkeningThresholds();
+        }
 
-            if (brighteningScreenIdle != null
-                    && brighteningScreenIdle.getMinimum() != null) {
-                mScreenBrighteningMinThresholdIdle =
-                        brighteningScreenIdle.getMinimum().floatValue();
-            }
-            if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) {
-                mScreenDarkeningMinThresholdIdle =
-                        darkeningScreenIdle.getMinimum().floatValue();
-            }
+        Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningScreenIdle,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenBrighteningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+        mScreenBrighteningLevelsIdle = screenBrighteningPair.first;
+        mScreenBrighteningPercentagesIdle = screenBrighteningPair.second;
+
+        Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningScreenIdle,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenDarkeningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+        mScreenDarkeningLevelsIdle = screenDarkeningPair.first;
+        mScreenDarkeningPercentagesIdle = screenDarkeningPair.second;
+
+        if (brighteningScreenIdle != null
+                && brighteningScreenIdle.getMinimum() != null) {
+            mScreenBrighteningMinThresholdIdle =
+                    brighteningScreenIdle.getMinimum().floatValue();
+        }
+        if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) {
+            mScreenDarkeningMinThresholdIdle =
+                    darkeningScreenIdle.getMinimum().floatValue();
         }
     }
 
-    private void loadIdleAmbientBrightnessThresholds(Thresholds idleAmbientBrightnessThresholds) {
-        if (idleAmbientBrightnessThresholds != null) {
-            BrightnessThresholds brighteningAmbientLuxIdle =
-                    idleAmbientBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningAmbientLuxIdle =
-                    idleAmbientBrightnessThresholds.getDarkeningThresholds();
+    private void loadAmbientBrightnessThresholdsIdle(DisplayConfiguration config) {
+        BrightnessThresholds brighteningAmbientLuxIdle = null;
+        BrightnessThresholds darkeningAmbientLuxIdle = null;
+        if (config != null && config.getAmbientBrightnessChangeThresholdsIdle() != null) {
+            brighteningAmbientLuxIdle =
+                    config.getAmbientBrightnessChangeThresholdsIdle().getBrighteningThresholds();
+            darkeningAmbientLuxIdle =
+                    config.getAmbientBrightnessChangeThresholdsIdle().getDarkeningThresholds();
+        }
 
-            if (brighteningAmbientLuxIdle != null
-                    && brighteningAmbientLuxIdle.getMinimum() != null) {
-                mAmbientLuxBrighteningMinThresholdIdle =
-                        brighteningAmbientLuxIdle.getMinimum().floatValue();
+        Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningAmbientLuxIdle,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientBrighteningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS);
+        mAmbientBrighteningLevelsIdle = ambientBrighteningPair.first;
+        mAmbientBrighteningPercentagesIdle = ambientBrighteningPair.second;
+
+        Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningAmbientLuxIdle,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientDarkeningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS);
+        mAmbientDarkeningLevelsIdle = ambientDarkeningPair.first;
+        mAmbientDarkeningPercentagesIdle = ambientDarkeningPair.second;
+
+        if (brighteningAmbientLuxIdle != null
+                && brighteningAmbientLuxIdle.getMinimum() != null) {
+            mAmbientLuxBrighteningMinThresholdIdle =
+                    brighteningAmbientLuxIdle.getMinimum().floatValue();
+        }
+
+        if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) {
+            mAmbientLuxDarkeningMinThresholdIdle =
+                    darkeningAmbientLuxIdle.getMinimum().floatValue();
+        }
+    }
+
+    private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds,
+            int configFallbackThreshold, int configFallbackPercentage, float[] defaultLevels,
+            float[] defaultPercentage) {
+        return getBrightnessLevelAndPercentage(thresholds, configFallbackThreshold,
+                configFallbackPercentage, defaultLevels, defaultPercentage, false);
+    }
+
+    // Returns two float arrays, one of the brightness levels and one of the corresponding threshold
+    // percentages for brightness levels at or above the lux value.
+    // Historically, config.xml would have an array for brightness levels that was 1 shorter than
+    // the levels array. Now we prepend a 0 to this array so they can be treated the same in the
+    // rest of the framework. Values were also defined in different units (permille vs percent).
+    private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds,
+            int configFallbackThreshold, int configFallbackPermille,
+            float[] defaultLevels, float[] defaultPercentage,
+            boolean potentialOldBrightnessScale) {
+        if (thresholds != null
+                && thresholds.getBrightnessThresholdPoints() != null
+                && thresholds.getBrightnessThresholdPoints()
+                        .getBrightnessThresholdPoint().size() != 0) {
+
+            // The level and percentages arrays are equal length in the ddc (new system)
+            List<ThresholdPoint> points =
+                    thresholds.getBrightnessThresholdPoints().getBrightnessThresholdPoint();
+            final int size = points.size();
+
+            float[] thresholdLevels = new float[size];
+            float[] thresholdPercentages = new float[size];
+
+            int i = 0;
+            for (ThresholdPoint point : points) {
+                thresholdLevels[i] = point.getThreshold().floatValue();
+                thresholdPercentages[i] = point.getPercentage().floatValue();
+                i++;
             }
-            if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) {
-                mAmbientLuxDarkeningMinThresholdIdle =
-                        darkeningAmbientLuxIdle.getMinimum().floatValue();
+            return new Pair<>(thresholdLevels, thresholdPercentages);
+        } else {
+            // The level and percentages arrays are unequal length in config.xml (old system)
+            // We prefix the array with a 0 value to ensure they can be handled consistently
+            // with the new system.
+
+            // Load levels array
+            int[] configThresholdArray = mContext.getResources().getIntArray(
+                    configFallbackThreshold);
+            int configThresholdsSize;
+            if (configThresholdArray == null || configThresholdArray.length == 0) {
+                configThresholdsSize = 1;
+            } else {
+                configThresholdsSize = configThresholdArray.length + 1;
+            }
+
+
+            // Load percentage array
+            int[] configPermille = mContext.getResources().getIntArray(
+                    configFallbackPermille);
+
+            // Ensure lengths match up
+            boolean emptyArray = configPermille == null || configPermille.length == 0;
+            if (emptyArray && configThresholdsSize == 1) {
+                return new Pair<>(defaultLevels, defaultPercentage);
+            }
+            if (emptyArray || configPermille.length != configThresholdsSize) {
+                throw new IllegalArgumentException(
+                        "Brightness threshold arrays do not align in length");
+            }
+
+            // Calculate levels array
+            float[] configThresholdWithZeroPrefixed = new float[configThresholdsSize];
+            // Start at 1, so that 0 index value is 0.0f (default)
+            for (int i = 1; i < configThresholdsSize; i++) {
+                configThresholdWithZeroPrefixed[i] = (float) configThresholdArray[i - 1];
+            }
+            if (potentialOldBrightnessScale) {
+                configThresholdWithZeroPrefixed =
+                        constraintInRangeIfNeeded(configThresholdWithZeroPrefixed);
+            }
+
+            // Calculate percentages array
+            float[] configPercentage = new float[configThresholdsSize];
+            for (int i = 0; i < configPermille.length; i++) {
+                configPercentage[i] = configPermille[i] / 10.0f;
+            }            return new Pair<>(configThresholdWithZeroPrefixed, configPercentage);
+        }
+    }
+
+    /**
+     * This check is due to historical reasons, where screen thresholdLevels used to be
+     * integer values in the range of [0-255], but then was changed to be float values from [0,1].
+     * To accommodate both the possibilities, we first check if all the thresholdLevels are in
+     * [0,1], and if not, we divide all the levels with 255 to bring them down to the same scale.
+     */
+    private float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
+        if (isAllInRange(thresholdLevels, /* minValueInclusive= */ 0.0f,
+                /* maxValueInclusive= */ 1.0f)) {
+            return thresholdLevels;
+        }
+
+        Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
+        float[] thresholdLevelsScaled = new float[thresholdLevels.length];
+        for (int index = 0; thresholdLevels.length > index; ++index) {
+            thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
+        }
+        return thresholdLevelsScaled;
+    }
+
+    private boolean isAllInRange(float[] configArray, float minValueInclusive,
+            float maxValueInclusive) {
+        for (float v : configArray) {
+            if (v < minValueInclusive || v > maxValueInclusive) {
+                return false;
             }
         }
+        return true;
     }
 
     private boolean thermalStatusIsValid(ThermalStatus value) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 52d630b..4752044 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -970,53 +970,77 @@
                     com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
-            int[] ambientBrighteningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_ambientBrighteningThresholds);
-            int[] ambientDarkeningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_ambientDarkeningThresholds);
-            int[] ambientThresholdLevels = resources.getIntArray(
-                    com.android.internal.R.array.config_ambientThresholdLevels);
+            // Ambient Lux - Active Mode Brightness Thresholds
+            float[] ambientBrighteningThresholds =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+            float[] ambientDarkeningThresholds =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+            float[] ambientBrighteningLevels =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
+            float[] ambientDarkeningLevels =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
             float ambientDarkeningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
             float ambientBrighteningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
             HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
                     ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientThresholdLevels, ambientDarkeningMinThreshold,
+                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
                     ambientBrighteningMinThreshold);
 
-            int[] screenBrighteningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_screenBrighteningThresholds);
-            int[] screenDarkeningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_screenDarkeningThresholds);
-            float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels));
+            // Display - Active Mode Brightness Thresholds
+            float[] screenBrighteningThresholds =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
+            float[] screenDarkeningThresholds =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
+            float[] screenBrighteningLevels =
+                    mDisplayDeviceConfig.getScreenBrighteningLevels();
+            float[] screenDarkeningLevels =
+                    mDisplayDeviceConfig.getScreenDarkeningLevels();
             float screenDarkeningMinThreshold =
                     mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
             float screenBrighteningMinThreshold =
                     mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
             HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
-                    screenDarkeningMinThreshold, screenBrighteningMinThreshold);
+                    screenBrighteningThresholds, screenDarkeningThresholds,
+                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+                    screenBrighteningMinThreshold, true);
 
-            // Idle screen thresholds
-            float screenDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
-            float screenBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
-                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
-            // Idle ambient thresholds
+            // Ambient Lux - Idle Screen Brightness Thresholds
             float ambientDarkeningMinThresholdIdle =
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
             float ambientBrighteningMinThresholdIdle =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+            float[] ambientBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+            float[] ambientDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+            float[] ambientBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+            float[] ambientDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
             HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
-                    ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientThresholdLevels, ambientDarkeningMinThresholdIdle,
-                    ambientBrighteningMinThresholdIdle);
+                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
+
+            // Display - Idle Screen Brightness Thresholds
+            float screenDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+            float screenBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+            float[] screenBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+            float[] screenDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+            float[] screenBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+            float[] screenDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
 
             long brighteningLightDebounce = mDisplayDeviceConfig
                     .getAutoBrightnessBrighteningLightDebounce();
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index db4cd7f..172b4be 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -946,53 +946,77 @@
                     R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
-            int[] ambientBrighteningThresholds = resources.getIntArray(
-                    R.array.config_ambientBrighteningThresholds);
-            int[] ambientDarkeningThresholds = resources.getIntArray(
-                    R.array.config_ambientDarkeningThresholds);
-            int[] ambientThresholdLevels = resources.getIntArray(
-                    R.array.config_ambientThresholdLevels);
+            // Ambient Lux - Active Mode Brightness Thresholds
+            float[] ambientBrighteningThresholds =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+            float[] ambientDarkeningThresholds =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+            float[] ambientBrighteningLevels =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
+            float[] ambientDarkeningLevels =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
             float ambientDarkeningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
             float ambientBrighteningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
             HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
                     ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientThresholdLevels, ambientDarkeningMinThreshold,
+                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
                     ambientBrighteningMinThreshold);
 
-            int[] screenBrighteningThresholds = resources.getIntArray(
-                    R.array.config_screenBrighteningThresholds);
-            int[] screenDarkeningThresholds = resources.getIntArray(
-                    R.array.config_screenDarkeningThresholds);
-            float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels));
+            // Display - Active Mode Brightness Thresholds
+            float[] screenBrighteningThresholds =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
+            float[] screenDarkeningThresholds =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
+            float[] screenBrighteningLevels =
+                    mDisplayDeviceConfig.getScreenBrighteningLevels();
+            float[] screenDarkeningLevels =
+                    mDisplayDeviceConfig.getScreenDarkeningLevels();
             float screenDarkeningMinThreshold =
                     mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
             float screenBrighteningMinThreshold =
                     mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
             HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
-                    screenDarkeningMinThreshold, screenBrighteningMinThreshold);
+                    screenBrighteningThresholds, screenDarkeningThresholds,
+                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+                    screenBrighteningMinThreshold, true);
 
-            // Idle screen thresholds
-            float screenDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
-            float screenBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
-                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
-            // Idle ambient thresholds
+            // Ambient Lux - Idle Screen Brightness Thresholds
             float ambientDarkeningMinThresholdIdle =
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
             float ambientBrighteningMinThresholdIdle =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+            float[] ambientBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+            float[] ambientDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+            float[] ambientBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+            float[] ambientDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
             HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
-                    ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientThresholdLevels, ambientDarkeningMinThresholdIdle,
-                    ambientBrighteningMinThresholdIdle);
+                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
+
+            // Display - Idle Screen Brightness Thresholds
+            float screenDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+            float screenBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+            float[] screenBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+            float[] screenDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+            float[] screenBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+            float[] screenDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
 
             long brighteningLightDebounce = mDisplayDeviceConfig
                     .getAutoBrightnessBrighteningLightDebounce();
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index abf8fe3..3c522e7 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -29,61 +29,60 @@
 
     private static final boolean DEBUG = false;
 
-    private final float[] mBrighteningThresholds;
-    private final float[] mDarkeningThresholds;
-    private final float[] mThresholdLevels;
+    private final float[] mBrighteningThresholdsPercentages;
+    private final float[] mDarkeningThresholdsPercentages;
+    private final float[] mBrighteningThresholdLevels;
+    private final float[] mDarkeningThresholdLevels;
     private final float mMinDarkening;
     private final float mMinBrightening;
 
     /**
-     * Creates a {@code HysteresisLevels} object for ambient brightness.
-     * @param brighteningThresholds an array of brightening hysteresis constraint constants.
-     * @param darkeningThresholds an array of darkening hysteresis constraint constants.
-     * @param thresholdLevels a monotonically increasing array of threshold levels.
+     * Creates a {@code HysteresisLevels} object with the given equal-length
+     * float arrays.
+     * @param brighteningThresholdsPercentages 0-100 of thresholds
+     * @param darkeningThresholdsPercentages 0-100 of thresholds
+     * @param brighteningThresholdLevels float array of brightness values in the relevant units
      * @param minBrighteningThreshold the minimum value for which the brightening value needs to
      *                                return.
      * @param minDarkeningThreshold the minimum value for which the darkening value needs to return.
+     * @param potentialOldBrightnessRange whether or not the values used could be from the old
+     *                                    screen brightness range ie, between 1-255.
     */
-    HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds,
-            int[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) {
-        if (brighteningThresholds.length != darkeningThresholds.length
-                || darkeningThresholds.length != thresholdLevels.length + 1) {
+    HysteresisLevels(float[] brighteningThresholdsPercentages,
+            float[] darkeningThresholdsPercentages,
+            float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
+            float minDarkeningThreshold, float minBrighteningThreshold,
+            boolean potentialOldBrightnessRange) {
+        if (brighteningThresholdsPercentages.length != brighteningThresholdLevels.length
+                || darkeningThresholdsPercentages.length != darkeningThresholdLevels.length) {
             throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
         }
-        mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f);
-        mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f);
-        mThresholdLevels = setArrayFormat(thresholdLevels, 1.0f);
+        mBrighteningThresholdsPercentages =
+                setArrayFormat(brighteningThresholdsPercentages, 100.0f);
+        mDarkeningThresholdsPercentages =
+                setArrayFormat(darkeningThresholdsPercentages, 100.0f);
+        mBrighteningThresholdLevels = setArrayFormat(brighteningThresholdLevels, 1.0f);
+        mDarkeningThresholdLevels = setArrayFormat(darkeningThresholdLevels, 1.0f);
         mMinDarkening = minDarkeningThreshold;
         mMinBrightening = minBrighteningThreshold;
     }
 
-    /**
-     * Creates a {@code HysteresisLevels} object for screen brightness.
-     * @param brighteningThresholds an array of brightening hysteresis constraint constants.
-     * @param darkeningThresholds an array of darkening hysteresis constraint constants.
-     * @param thresholdLevels a monotonically increasing array of threshold levels.
-     * @param minBrighteningThreshold the minimum value for which the brightening value needs to
-     *                                return.
-     * @param minDarkeningThreshold the minimum value for which the darkening value needs to return.
-     */
-    HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds,
-            float[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) {
-        if (brighteningThresholds.length != darkeningThresholds.length
-                || darkeningThresholds.length != thresholdLevels.length + 1) {
-            throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
-        }
-        mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f);
-        mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f);
-        mThresholdLevels = constraintInRangeIfNeeded(thresholdLevels);
-        mMinDarkening = minDarkeningThreshold;
-        mMinBrightening = minBrighteningThreshold;
+    HysteresisLevels(float[] brighteningThresholdsPercentages,
+            float[] darkeningThresholdsPercentages,
+            float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
+            float minDarkeningThreshold, float minBrighteningThreshold) {
+        this(brighteningThresholdsPercentages, darkeningThresholdsPercentages,
+                brighteningThresholdLevels, darkeningThresholdLevels, minDarkeningThreshold,
+                minBrighteningThreshold, false);
     }
 
     /**
      * Return the brightening hysteresis threshold for the given value level.
      */
     public float getBrighteningThreshold(float value) {
-        final float brightConstant = getReferenceLevel(value, mBrighteningThresholds);
+        final float brightConstant = getReferenceLevel(value,
+                mBrighteningThresholdLevels, mBrighteningThresholdsPercentages);
+
         float brightThreshold = value * (1.0f + brightConstant);
         if (DEBUG) {
             Slog.d(TAG, "bright hysteresis constant=" + brightConstant + ", threshold="
@@ -98,7 +97,8 @@
      * Return the darkening hysteresis threshold for the given value level.
      */
     public float getDarkeningThreshold(float value) {
-        final float darkConstant = getReferenceLevel(value, mDarkeningThresholds);
+        final float darkConstant = getReferenceLevel(value,
+                mDarkeningThresholdLevels, mDarkeningThresholdsPercentages);
         float darkThreshold = value * (1.0f - darkConstant);
         if (DEBUG) {
             Slog.d(TAG, "dark hysteresis constant=: " + darkConstant + ", threshold="
@@ -111,60 +111,38 @@
     /**
      * Return the hysteresis constant for the closest threshold value from the given array.
      */
-    private float getReferenceLevel(float value, float[] referenceLevels) {
-        int index = 0;
-        while (mThresholdLevels.length > index && value >= mThresholdLevels[index]) {
-            ++index;
+    private float getReferenceLevel(float value, float[] thresholdLevels,
+            float[] thresholdPercentages) {
+        if (thresholdLevels == null || thresholdLevels.length == 0 || value < thresholdLevels[0]) {
+            return 0.0f;
         }
-        return referenceLevels[index];
+        int index = 0;
+        while (index < thresholdLevels.length - 1 && value >= thresholdLevels[index + 1]) {
+            index++;
+        }
+        return thresholdPercentages[index];
     }
 
     /**
      * Return a float array where each i-th element equals {@code configArray[i]/divideFactor}.
      */
-    private float[] setArrayFormat(int[] configArray, float divideFactor) {
+    private float[] setArrayFormat(float[] configArray, float divideFactor) {
         float[] levelArray = new float[configArray.length];
         for (int index = 0; levelArray.length > index; ++index) {
-            levelArray[index] = (float) configArray[index] / divideFactor;
+            levelArray[index] = configArray[index] / divideFactor;
         }
         return levelArray;
     }
 
-    /**
-     * This check is due to historical reasons, where screen thresholdLevels used to be
-     * integer values in the range of [0-255], but then was changed to be float values from [0,1].
-     * To accommodate both the possibilities, we first check if all the thresholdLevels are in [0,
-     * 1], and if not, we divide all the levels with 255 to bring them down to the same scale.
-     */
-    private float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
-        if (isAllInRange(thresholdLevels, /* minValueInclusive = */ 0.0f, /* maxValueInclusive = */
-                1.0f)) {
-            return thresholdLevels;
-        }
-
-        Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
-        float[] thresholdLevelsScaled = new float[thresholdLevels.length];
-        for (int index = 0; thresholdLevels.length > index; ++index) {
-            thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
-        }
-        return thresholdLevelsScaled;
-    }
-
-    private boolean isAllInRange(float[] configArray, float minValueInclusive,
-            float maxValueInclusive) {
-        int configArraySize = configArray.length;
-        for (int index = 0; configArraySize > index; ++index) {
-            if (configArray[index] < minValueInclusive || configArray[index] > maxValueInclusive) {
-                return false;
-            }
-        }
-        return true;
-    }
-
     void dump(PrintWriter pw) {
         pw.println("HysteresisLevels");
-        pw.println("  mBrighteningThresholds=" + Arrays.toString(mBrighteningThresholds));
-        pw.println("  mDarkeningThresholds=" + Arrays.toString(mDarkeningThresholds));
-        pw.println("  mThresholdLevels=" + Arrays.toString(mThresholdLevels));
+        pw.println("  mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels));
+        pw.println("  mBrighteningThresholdsPercentages="
+                + Arrays.toString(mBrighteningThresholdsPercentages));
+        pw.println("  mMinBrightening=" + mMinBrightening);
+        pw.println("  mDarkeningThresholdLevels=" + Arrays.toString(mDarkeningThresholdLevels));
+        pw.println("  mDarkeningThresholdsPercentages="
+                + Arrays.toString(mDarkeningThresholdsPercentages));
+        pw.println("  mMinDarkening=" + mMinDarkening);
     }
 }
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 3e39746..82436cc 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -221,14 +221,12 @@
     }
 
     @AnyThread
-    boolean updateEditorToolType(int toolType) {
+    void updateEditorToolType(@MotionEvent.ToolType int toolType) {
         try {
             mTarget.updateEditorToolType(toolType);
         } catch (RemoteException e) {
             logRemoteException(e);
-            return false;
         }
-        return true;
     }
 
     @AnyThread
diff --git a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
index 5a0069a..789222e 100644
--- a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
+++ b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
@@ -1,18 +1,18 @@
 /*
-** Copyright 2016, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
 package com.android.server.inputmethod;
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
index 2160b65..dc2799e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
@@ -29,7 +29,7 @@
     private boolean mHideImeWhenNoEditorFocus;
     private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
 
-    public InputMethodDeviceConfigs() {
+    InputMethodDeviceConfigs() {
         mDeviceConfigChangedListener = properties -> {
             if (!DeviceConfig.NAMESPACE_INPUT_METHOD_MANAGER.equals(properties.getNamespace())) {
                 return;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8a56afa..520a471 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -69,7 +69,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.AppGlobals;
-import android.app.AppOpsManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -307,7 +306,6 @@
     final boolean mHasFeature;
     private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap =
             new ArrayMap<>();
-    private final AppOpsManager mAppOpsManager;
     private final UserManager mUserManager;
     private final UserManagerInternal mUserManagerInternal;
     private final InputMethodMenuController mMenuController;
@@ -1734,7 +1732,6 @@
         mInputMethodDeviceConfigs = new InputMethodDeviceConfigs();
         mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy;
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
-        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
         mUserManager = mContext.getSystemService(UserManager.class);
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         mAccessibilityManager = AccessibilityManager.getInstance(context);
@@ -2520,7 +2517,7 @@
                     null, null, null, selectedMethodId, getSequenceNumberLocked(), null, false);
         }
 
-        if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.mUid,
+        if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid,
                 editorInfo.packageName)) {
             Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
                     + " uid=" + cs.mUid + " package=" + editorInfo.packageName);
@@ -3957,7 +3954,7 @@
             return false;
         }
         if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid(
-                mAppOpsManager,
+                mPackageManagerInternal,
                 uid,
                 getCurIntentLocked().getComponent().getPackageName())) {
             return true;
@@ -4156,6 +4153,7 @@
         if (UserHandle.getCallingUserId() != userId) {
             mContext.enforceCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
         }
+        final int callingUid = Binder.getCallingUid();
 
         // By this IPC call, only a process which shares the same uid with the IME can add
         // additional input method subtypes to the IME.
@@ -4176,7 +4174,7 @@
 
             if (mSettings.getCurrentUserId() == userId) {
                 if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded,
-                        mAdditionalSubtypeMap, mIPackageManager)) {
+                        mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) {
                     return;
                 }
                 final long ident = Binder.clearCallingIdentity();
@@ -4188,14 +4186,17 @@
                 return;
             }
 
-            final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
-            final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
-                    userId, false);
+            final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+            final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
             final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
                     new ArrayMap<>();
             AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+            queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
+                    methodList, DirectBootAwareness.AUTO);
+            final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+                    userId, false);
             settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
-                    mIPackageManager);
+                    mPackageManagerInternal, callingUid);
         }
     }
 
@@ -4208,8 +4209,8 @@
         final int callingUid = Binder.getCallingUid();
         final ComponentName imeComponentName =
                 imeId != null ? ComponentName.unflattenFromString(imeId) : null;
-        if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager,
-                callingUid, imeComponentName.getPackageName())) {
+        if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(
+                mPackageManagerInternal, callingUid, imeComponentName.getPackageName())) {
             throw new SecurityException("Calling UID=" + callingUid + " does not belong to imeId="
                     + imeId);
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index 11e6923..a25630f 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -71,7 +71,7 @@
     @Nullable
     private InputMethodDialogWindowContext mDialogWindowContext;
 
-    public InputMethodMenuController(InputMethodManagerService service) {
+    InputMethodMenuController(InputMethodManagerService service) {
         mService = service;
         mSettings = mService.mSettings;
         mSwitchingController = mService.mSwitchingController;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index c57fe33..69b0661 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -20,16 +20,13 @@
 import android.annotation.Nullable;
 import android.annotation.UserHandleAware;
 import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.res.Resources;
-import android.os.Binder;
 import android.os.Build;
-import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -45,7 +42,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.inputmethod.StartInputFlags;
-import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.textservices.TextServicesManagerInternal;
@@ -80,29 +76,6 @@
     }
 
     // ----------------------------------------------------------------------
-    // Utilities for debug
-    static String getApiCallStack() {
-        String apiCallStack = "";
-        try {
-            throw new RuntimeException();
-        } catch (RuntimeException e) {
-            final StackTraceElement[] frames = e.getStackTrace();
-            for (int j = 1; j < frames.length; ++j) {
-                final String tempCallStack = frames[j].toString();
-                if (TextUtils.isEmpty(apiCallStack)) {
-                    // Overwrite apiCallStack if it's empty
-                    apiCallStack = tempCallStack;
-                } else if (tempCallStack.indexOf("Transact(") < 0) {
-                    // Overwrite apiCallStack if it's not a binder call
-                    apiCallStack = tempCallStack;
-                } else {
-                    break;
-                }
-            }
-        }
-        return apiCallStack;
-    }
-    // ----------------------------------------------------------------------
 
     static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
         if (subtype == null) return true;
@@ -210,28 +183,27 @@
         return subtype != null
                 ? TextUtils.concat(subtype.getDisplayName(context,
                         imi.getPackageName(), imi.getServiceInfo().applicationInfo),
-                                (TextUtils.isEmpty(imiLabel) ?
-                                        "" : " - " + imiLabel))
+                                (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel))
                 : imiLabel;
     }
 
     /**
      * Returns true if a package name belongs to a UID.
      *
-     * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
-     * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
+     * <p>This is a simple wrapper of
+     * {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p>
+     * @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the
+     *                               validation.
      * @param uid the UID to be validated.
      * @param packageName the package name.
      * @return {@code true} if the package name belongs to the UID.
      */
-    static boolean checkIfPackageBelongsToUid(AppOpsManager appOpsManager,
+    static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal,
             int uid, String packageName) {
-        try {
-            appOpsManager.checkPackage(uid, packageName);
-            return true;
-        } catch (SecurityException e) {
-            return false;
-        }
+        // PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of
+        // writing. So setting 0 should be fine.
+        return packageManagerInternal.getPackageUid(packageName, 0 /* flags */,
+                UserHandle.getUserId(uid)) == uid;
     }
 
     /**
@@ -674,8 +646,8 @@
                             List<InputMethodSubtype> implicitlyEnabledSubtypes =
                                     SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi);
                             if (implicitlyEnabledSubtypes != null) {
-                                final int N = implicitlyEnabledSubtypes.size();
-                                for (int i = 0; i < N; ++i) {
+                                final int numSubtypes = implicitlyEnabledSubtypes.size();
+                                for (int i = 0; i < numSubtypes; ++i) {
                                     final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
                                     if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
                                         return subtypeHashCode;
@@ -877,20 +849,13 @@
         boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
                 @NonNull ArrayList<InputMethodSubtype> subtypes,
                 @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
-                @NonNull IPackageManager packageManager) {
+                @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
             final InputMethodInfo imi = mMethodMap.get(imeId);
             if (imi == null) {
                 return false;
             }
-            final String[] packageInfos;
-            try {
-                packageInfos = packageManager.getPackagesForUid(Binder.getCallingUid());
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to get package infos");
-                return false;
-            }
-            if (ArrayUtils.find(packageInfos,
-                    packageInfo -> TextUtils.equals(packageInfo, imi.getPackageName())) == null) {
+            if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
+                    imi.getPackageName())) {
                 return false;
             }
 
diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
index 3d02b3a..f865e60 100644
--- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java
+++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
@@ -46,7 +46,7 @@
      * @param desired The locale preferred by user.
      * @return A score based on the locale matching for the default subtype enabling.
      */
-    @IntRange(from=1, to=3)
+    @IntRange(from = 1, to = 3)
     private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
             @NonNull final ULocale desired) {
         // Assuming supported/desired is fully expanded.
@@ -111,7 +111,7 @@
          * @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
          * {@code right}. 0 if {@code left} and {@code right} is equal.
          */
-        @IntRange(from=-1, to=1)
+        @IntRange(from = -1, to = 1)
         private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
             for (int i = 0; i < left.length; ++i) {
                 if (left[i] > right[i]) {
diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
index 7085868..f07539f 100644
--- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
@@ -68,14 +68,14 @@
         if (locale == null) {
             return false;
         }
-        final int N = imi.getSubtypeCount();
-        for (int i = 0; i < N; ++i) {
+        final int numSubtypes = imi.getSubtypeCount();
+        for (int i = 0; i < numSubtypes; ++i) {
             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
             if (checkCountry) {
                 final Locale subtypeLocale = subtype.getLocaleObject();
-                if (subtypeLocale == null ||
-                        !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
-                        !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
+                if (subtypeLocale == null
+                        || !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())
+                        || !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
                     continue;
                 }
             } else {
@@ -260,8 +260,8 @@
         boolean partialMatchFound = false;
         InputMethodSubtype applicableSubtype = null;
         InputMethodSubtype firstMatchedModeSubtype = null;
-        final int N = subtypes.size();
-        for (int i = 0; i < N; ++i) {
+        final int numSubtypes = subtypes.size();
+        for (int i = 0; i < numSubtypes; ++i) {
             InputMethodSubtype subtype = subtypes.get(i);
             final String subtypeLocale = subtype.getLocale();
             final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 533d1b0..58d677c 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -113,6 +113,7 @@
 import android.util.LongSparseArray;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -141,6 +142,7 @@
 import com.android.server.locksettings.SyntheticPasswordManager.TokenType;
 import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
 import com.android.server.wm.WindowManagerInternal;
 
 import libcore.util.HexEncoding;
@@ -243,6 +245,17 @@
 
     private final RebootEscrowManager mRebootEscrowManager;
 
+    // Locking order is mUserCreationAndRemovalLock -> mSpManager.
+    private final Object mUserCreationAndRemovalLock = new Object();
+    // These two arrays are only used at boot time.  To save memory, they are set to null when
+    // PHASE_BOOT_COMPLETED is reached.
+    @GuardedBy("mUserCreationAndRemovalLock")
+    private SparseIntArray mEarlyCreatedUsers = new SparseIntArray();
+    @GuardedBy("mUserCreationAndRemovalLock")
+    private SparseIntArray mEarlyRemovedUsers = new SparseIntArray();
+    @GuardedBy("mUserCreationAndRemovalLock")
+    private boolean mBootComplete;
+
     // Current password metric for all users on the device. Updated when user unlocks
     // the device or changes password. Removed when user is stopped.
     @GuardedBy("this")
@@ -283,9 +296,16 @@
         @Override
         public void onBootPhase(int phase) {
             super.onBootPhase(phase);
-            if (phase == PHASE_ACTIVITY_MANAGER_READY) {
-                mLockSettingsService.migrateOldDataAfterSystemReady();
-                mLockSettingsService.loadEscrowData();
+            switch (phase) {
+                case PHASE_ACTIVITY_MANAGER_READY:
+                    mLockSettingsService.migrateOldDataAfterSystemReady();
+                    mLockSettingsService.loadEscrowData();
+                    break;
+                case PHASE_BOOT_COMPLETED:
+                    mLockSettingsService.bootCompleted();
+                    break;
+                default:
+                    break;
             }
         }
 
@@ -577,7 +597,6 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_USER_ADDED);
         filter.addAction(Intent.ACTION_USER_STARTING);
-        filter.addAction(Intent.ACTION_USER_REMOVED);
         injector.getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
                 null, null);
 
@@ -720,28 +739,32 @@
     }
 
     /**
-     * Clean up states associated with the given user, in case the userId is reused but LSS didn't
-     * get a chance to do cleanup previously during ACTION_USER_REMOVED.
-     *
-     * Internally, LSS stores serial number for each user and check it against the current user's
-     * serial number to determine if the userId is reused and invoke cleanup code.
+     * Removes the LSS state for the given userId if the userId was reused without its LSS state
+     * being fully removed.
+     * <p>
+     * This is primarily needed for users that were removed by Android 13 or earlier, which didn't
+     * guarantee removal of LSS state as it relied on the {@code ACTION_USER_REMOVED} intent.  It is
+     * also needed because {@link #removeUser()} delays requests to remove LSS state until the
+     * {@code PHASE_BOOT_COMPLETED} boot phase, so they can be lost.
+     * <p>
+     * Stale state is detected by checking whether the user serial number changed.  This works
+     * because user serial numbers are never reused.
      */
-    private void cleanupDataForReusedUserIdIfNecessary(int userId) {
+    private void removeStateForReusedUserIdIfNecessary(@UserIdInt int userId, int serialNumber) {
         if (userId == UserHandle.USER_SYSTEM) {
             // Short circuit as we never clean up user 0.
             return;
         }
-        // Serial number is never reusued, so we can use it as a distinguisher for user Id reuse.
-        int serialNumber = mUserManager.getUserSerialNumber(userId);
-
         int storedSerialNumber = mStorage.getInt(USER_SERIAL_NUMBER_KEY, -1, userId);
         if (storedSerialNumber != serialNumber) {
             // If LockSettingsStorage does not have a copy of the serial number, it could be either
             // this is a user created before the serial number recording logic is introduced, or
             // the user does not exist or was removed and cleaned up properly. In either case, don't
-            // invoke removeUser().
+            // invoke removeUserState().
             if (storedSerialNumber != -1) {
-                removeUser(userId, /* unknownUser */ true);
+                Slogf.i(TAG, "Removing stale state for reused userId %d (serial %d => %d)", userId,
+                        storedSerialNumber, serialNumber);
+                removeUserState(userId);
             }
             mStorage.setInt(USER_SERIAL_NUMBER_KEY, serialNumber, userId);
         }
@@ -771,7 +794,6 @@
         mHandler.post(new Runnable() {
             @Override
             public void run() {
-                cleanupDataForReusedUserIdIfNecessary(userId);
                 ensureProfileKeystoreUnlocked(userId);
                 // Hide notification first, as tie managed profile lock takes time
                 hideEncryptionNotification(new UserHandle(userId));
@@ -779,38 +801,10 @@
                 if (isCredentialSharableWithParent(userId)) {
                     tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
                 }
-
-                // If the user doesn't have a credential, try and derive their secret for the
-                // AuthSecret HAL. The secret will have been enrolled if the user previously set a
-                // credential and still needs to be passed to the HAL once that credential is
-                // removed.
-                if (mUserManager.getUserInfo(userId).isPrimary() && !isUserSecure(userId)) {
-                    tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(userId);
-                }
             }
         });
     }
 
-    private void tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(@UserIdInt int userId) {
-        synchronized (mSpManager) {
-            // If there is no SP, then there is no vendor auth secret.
-            if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
-                return;
-            }
-
-            final long protectorId = getCurrentLskfBasedProtectorId(userId);
-            AuthenticationResult result =
-                    mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
-                            LockscreenCredential.createNone(), userId, null);
-            if (result.syntheticPassword != null) {
-                Slog.i(TAG, "Unwrapped SP for unsecured primary user " + userId);
-                onSyntheticPasswordKnown(userId, result.syntheticPassword);
-            } else {
-                Slog.e(TAG, "Failed to unwrap SP for unsecured primary user " + userId);
-            }
-        }
-    }
-
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -821,11 +815,6 @@
             } else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) {
                 final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
                 mStorage.prefetchUser(userHandle);
-            } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
-                final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
-                if (userHandle > 0) {
-                    removeUser(userHandle, /* unknownUser= */ false);
-                }
             }
         }
     };
@@ -937,6 +926,79 @@
         return success;
     }
 
+    private void bootCompleted() {
+        synchronized (mUserCreationAndRemovalLock) {
+            // Handle delayed calls to LSS.removeUser() and LSS.createNewUser().
+            for (int i = 0; i < mEarlyRemovedUsers.size(); i++) {
+                int userId = mEarlyRemovedUsers.keyAt(i);
+                Slogf.i(TAG, "Removing locksettings state for removed user %d now that boot "
+                        + "is complete", userId);
+                removeUserState(userId);
+            }
+            mEarlyRemovedUsers = null; // no longer needed
+            for (int i = 0; i < mEarlyCreatedUsers.size(); i++) {
+                int userId = mEarlyCreatedUsers.keyAt(i);
+                int serialNumber = mEarlyCreatedUsers.valueAt(i);
+
+                removeStateForReusedUserIdIfNecessary(userId, serialNumber);
+                synchronized (mSpManager) {
+                    if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
+                        Slogf.i(TAG, "Creating locksettings state for user %d now that boot "
+                                + "is complete", userId);
+                        initializeSyntheticPasswordLocked(userId);
+                    }
+                }
+            }
+            mEarlyCreatedUsers = null; // no longer needed
+
+            // Also do a one-time migration of all users to SP-based credentials with the CE key
+            // encrypted by the SP.  This is needed for the system user on the first boot of a
+            // device, as the system user is special and never goes through the user creation flow
+            // that other users do.  It is also needed for existing users on a device upgraded from
+            // Android 13 or earlier, where users with no LSKF didn't necessarily have an SP, and if
+            // they did have an SP then their CE key wasn't encrypted by it.
+            //
+            // If this gets interrupted (e.g. by the device powering off), there shouldn't be a
+            // problem since this will run again on the next boot, and setUserKeyProtection() is
+            // okay with the key being already protected by the given secret.
+            if (getString("migrated_all_users_to_sp_and_bound_ce", null, 0) == null) {
+                for (UserInfo user : mUserManager.getAliveUsers()) {
+                    removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
+                    synchronized (mSpManager) {
+                        migrateUserToSpWithBoundCeKeyLocked(user.id);
+                    }
+                }
+                setString("migrated_all_users_to_sp_and_bound_ce", "true", 0);
+            }
+
+            mBootComplete = true;
+        }
+    }
+
+    @GuardedBy("mSpManager")
+    private void migrateUserToSpWithBoundCeKeyLocked(@UserIdInt int userId) {
+        if (isUserSecure(userId)) {
+            Slogf.d(TAG, "User %d is secured; no migration needed", userId);
+            return;
+        }
+        long protectorId = getCurrentLskfBasedProtectorId(userId);
+        if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+            Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId);
+            initializeSyntheticPasswordLocked(userId);
+        } else {
+            Slogf.i(TAG, "Existing unsecured user %d has a synthetic password; re-encrypting CE " +
+                    "key with it", userId);
+            AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+                    getGateKeeperService(), protectorId, LockscreenCredential.createNone(), userId,
+                    null);
+            if (result.syntheticPassword == null) {
+                Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+                return;
+            }
+            setUserKeyProtection(userId, result.syntheticPassword.deriveFileBasedEncryptionKey());
+        }
+    }
+
     /**
      * Returns the lowest password quality that still presents the same UI for entering it.
      *
@@ -1269,9 +1331,8 @@
      * can end up calling into other system services to process user unlock request (via
      * {@link com.android.server.SystemServiceManager#unlockUser} </em>
      */
-    private void unlockUser(int userId, byte[] secret) {
-        Slog.i(TAG, "Unlocking user " + userId + " with secret only, length "
-                + (secret != null ? secret.length : 0));
+    private void unlockUser(@UserIdInt int userId) {
+        Slogf.i(TAG, "Unlocking user %d", userId);
         // TODO: make this method fully async so we can update UI with progress strings
         final boolean alreadyUnlocked = mUserManager.isUserUnlockingOrUnlocked(userId);
         final CountDownLatch latch = new CountDownLatch(1);
@@ -1294,7 +1355,7 @@
         };
 
         try {
-            mActivityManager.unlockUser(userId, null, secret, listener);
+            mActivityManager.unlockUser2(userId, listener);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
@@ -1587,6 +1648,7 @@
                 if (!savedCredential.isNone()) {
                     throw new IllegalStateException("Saved credential given, but user has no SP");
                 }
+                // TODO(b/232452368): this case is only needed by unit tests now; remove it.
                 initializeSyntheticPasswordLocked(userId);
             } else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
                 // get credential from keystore when profile has unified lock
@@ -1897,19 +1959,12 @@
         mStorage.writeChildProfileLock(userId, ArrayUtils.concat(iv, ciphertext));
     }
 
-    private void setUserKeyProtection(int userId, byte[] key) {
-        if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId);
-        addUserKeyAuth(userId, key);
-    }
-
-    private void clearUserKeyProtection(int userId, byte[] secret) {
-        if (DEBUG) Slog.d(TAG, "clearUserKeyProtection user=" + userId);
-        final UserInfo userInfo = mUserManager.getUserInfo(userId);
+    private void setUserKeyProtection(@UserIdInt int userId, byte[] secret) {
         final long callingId = Binder.clearCallingIdentity();
         try {
-            mStorageManager.clearUserKeyAuth(userId, userInfo.serialNumber, secret);
+            mStorageManager.setUserKeyProtection(userId, secret);
         } catch (RemoteException e) {
-            throw new IllegalStateException("clearUserKeyAuth failed user=" + userId);
+            throw new IllegalStateException("Failed to protect CE key for user " + userId, e);
         } finally {
             Binder.restoreCallingIdentity(callingId);
         }
@@ -1924,40 +1979,51 @@
         }
     }
 
-    /** Unlock file-based encryption */
-    private void unlockUserKey(int userId, byte[] secret) {
+    /**
+     * Unlocks the user's CE (credential-encrypted) storage if it's not already unlocked.
+     * <p>
+     * This method doesn't throw exceptions because it is called opportunistically whenever a user
+     * is started.  Whether it worked or not can be detected by whether the key got unlocked or not.
+     */
+    private void unlockUserKey(@UserIdInt int userId, SyntheticPassword sp) {
+        if (isUserKeyUnlocked(userId)) {
+            Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+            return;
+        }
         final UserInfo userInfo = mUserManager.getUserInfo(userId);
+        final String userType = isUserSecure(userId) ? "secured" : "unsecured";
+        final byte[] secret = sp.deriveFileBasedEncryptionKey();
         try {
             mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
+            Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId);
         } catch (RemoteException e) {
-            throw new IllegalStateException("Failed to unlock user key " + userId, e);
-
+            Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
+        } finally {
+            Arrays.fill(secret, (byte) 0);
         }
     }
 
-    private void addUserKeyAuth(int userId, byte[] secret) {
-        final UserInfo userInfo = mUserManager.getUserInfo(userId);
-        final long callingId = Binder.clearCallingIdentity();
-        try {
-            mStorageManager.addUserKeyAuth(userId, userInfo.serialNumber, secret);
-        } catch (RemoteException e) {
-            throw new IllegalStateException("Failed to add new key to vold " + userId, e);
-        } finally {
-            Binder.restoreCallingIdentity(callingId);
-        }
-    }
-
-    private void fixateNewestUserKeyAuth(int userId) {
-        if (DEBUG) Slog.d(TAG, "fixateNewestUserKeyAuth: user=" + userId);
-        final long callingId = Binder.clearCallingIdentity();
-        try {
-            mStorageManager.fixateNewestUserKeyAuth(userId);
-        } catch (RemoteException e) {
-            // OK to ignore the exception as vold would just accept both old and new
-            // keys if this call fails, and will fix itself during the next boot
-            Slog.w(TAG, "fixateNewestUserKeyAuth failed", e);
-        } finally {
-            Binder.restoreCallingIdentity(callingId);
+    private void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+        synchronized (mSpManager) {
+            if (isUserKeyUnlocked(userId)) {
+                Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+                return;
+            }
+            if (isUserSecure(userId)) {
+                Slogf.d(TAG, "Not unlocking CE storage for user %d yet because user is secured",
+                        userId);
+                return;
+            }
+            Slogf.i(TAG, "Unwrapping synthetic password for unsecured user %d", userId);
+            AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+                    getGateKeeperService(), getCurrentLskfBasedProtectorId(userId),
+                    LockscreenCredential.createNone(), userId, null);
+            if (result.syntheticPassword == null) {
+                Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+                return;
+            }
+            onSyntheticPasswordKnown(userId, result.syntheticPassword);
+            unlockUserKey(userId, result.syntheticPassword);
         }
     }
 
@@ -2228,8 +2294,50 @@
         });
     }
 
-    private void removeUser(int userId, boolean unknownUser) {
-        Slog.i(TAG, "RemoveUser: " + userId);
+    private void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+        synchronized (mUserCreationAndRemovalLock) {
+            // Before PHASE_BOOT_COMPLETED, don't actually create the synthetic password yet, but
+            // rather automatically delay it to later.  We do this because protecting the synthetic
+            // password requires the Weaver HAL if the device supports it, and some devices don't
+            // make Weaver available until fairly late in the boot process.  This logic ensures a
+            // consistent flow across all devices, regardless of their Weaver implementation.
+            if (!mBootComplete) {
+                Slogf.i(TAG, "Delaying locksettings state creation for user %d until boot complete",
+                        userId);
+                mEarlyCreatedUsers.put(userId, userSerialNumber);
+                mEarlyRemovedUsers.delete(userId);
+                return;
+            }
+            removeStateForReusedUserIdIfNecessary(userId, userSerialNumber);
+            synchronized (mSpManager) {
+                initializeSyntheticPasswordLocked(userId);
+            }
+        }
+    }
+
+    private void removeUser(@UserIdInt int userId) {
+        synchronized (mUserCreationAndRemovalLock) {
+            // Before PHASE_BOOT_COMPLETED, don't actually remove the LSS state yet, but rather
+            // automatically delay it to later.  We do this because deleting synthetic password
+            // protectors requires the Weaver HAL if the device supports it, and some devices don't
+            // make Weaver available until fairly late in the boot process.  This logic ensures a
+            // consistent flow across all devices, regardless of their Weaver implementation.
+            if (!mBootComplete) {
+                Slogf.i(TAG, "Delaying locksettings state removal for user %d until boot complete",
+                        userId);
+                if (mEarlyCreatedUsers.indexOfKey(userId) >= 0) {
+                    mEarlyCreatedUsers.delete(userId);
+                } else {
+                    mEarlyRemovedUsers.put(userId, -1 /* unused */);
+                }
+                return;
+            }
+            Slogf.i(TAG, "Removing state for user %d", userId);
+            removeUserState(userId);
+        }
+    }
+
+    private void removeUserState(@UserIdInt int userId) {
         removeBiometricsForUser(userId);
         mSpManager.removeUser(getGateKeeperService(), userId);
         mStrongAuth.removeUser(userId);
@@ -2238,11 +2346,9 @@
         mManagedProfilePasswordCache.removePassword(userId);
 
         gateKeeperClearSecureUserId(userId);
-        if (unknownUser || isCredentialSharableWithParent(userId)) {
-            removeKeystoreProfileKey(userId);
-        }
-        // Clean up storage last, this is to ensure that cleanupDataForReusedUserIdIfNecessary()
-        // can make the assumption that no USER_SERIAL_NUMBER_KEY means user is fully removed.
+        removeKeystoreProfileKey(userId);
+        // Clean up storage last, so that removeStateForReusedUserIdIfNecessary() can assume that no
+        // USER_SERIAL_NUMBER_KEY means user is fully removed.
         mStorage.removeUser(userId);
     }
 
@@ -2497,8 +2603,11 @@
     }
 
     private void callToAuthSecretIfNeeded(@UserIdInt int userId, SyntheticPassword sp) {
-        // Pass the primary user's auth secret to the HAL
-        if (mAuthSecretService != null && mUserManager.getUserInfo(userId).isPrimary()) {
+        // If the given user is the primary user, pass the auth secret to the HAL.  Only the system
+        // user can be primary.  Check for the system user ID before calling getUserInfo(), as other
+        // users may still be under construction.
+        if (mAuthSecretService != null && userId == UserHandle.USER_SYSTEM &&
+                mUserManager.getUserInfo(userId).isPrimary()) {
             try {
                 final byte[] rawSecret = sp.deriveVendorAuthSecret();
                 final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
@@ -2513,9 +2622,13 @@
     }
 
     /**
-     * Creates the synthetic password (SP) for the given user and protects it with an empty LSKF.
-     * This is called just once in the lifetime of the user: the first time a nonempty LSKF is set,
-     * or when an escrow token is activated on a device with an empty LSKF.
+     * Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and
+     * protects the user's CE key with a key derived from the SP.
+     * <p>
+     * This is called just once in the lifetime of the user: at user creation time (possibly delayed
+     * until {@code PHASE_BOOT_COMPLETED} to ensure that the Weaver HAL is available if the device
+     * supports it), or when upgrading from Android 13 or earlier where users with no LSKF didn't
+     * necessarily have an SP.
      */
     @GuardedBy("mSpManager")
     @VisibleForTesting
@@ -2529,6 +2642,7 @@
         final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
                 LockscreenCredential.createNone(), sp, userId);
         setCurrentLskfBasedProtectorId(protectorId, userId);
+        setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
         onSyntheticPasswordKnown(userId, sp);
         return sp;
     }
@@ -2601,11 +2715,10 @@
 
         unlockKeystore(sp.deriveKeyStorePassword(), userId);
 
-        {
-            final byte[] secret = sp.deriveFileBasedEncryptionKey();
-            unlockUser(userId, secret);
-            Arrays.fill(secret, (byte) 0);
-        }
+        unlockUserKey(userId, sp);
+
+        unlockUser(userId);
+
         activateEscrowTokens(sp, userId);
 
         if (isProfileWithSeparatedLock(userId)) {
@@ -2626,9 +2739,9 @@
      * be empty) and replacing the old LSKF-based protector with it.  The SP itself is not changed.
      *
      * Also maintains the invariants described in {@link SyntheticPasswordManager} by
-     * setting/clearing the protection (by the SP) on the user's file-based encryption key and
-     * auth-bound Keystore keys when the LSKF is added/removed, respectively.  If the new LSKF is
-     * nonempty, then the Gatekeeper auth token is also refreshed.
+     * setting/clearing the protection (by the SP) on the user's auth-bound Keystore keys when the
+     * LSKF is added/removed, respectively.  If the new LSKF is nonempty, then the Gatekeeper auth
+     * token is also refreshed.
      */
     @GuardedBy("mSpManager")
     private long setLockCredentialWithSpLocked(LockscreenCredential credential,
@@ -2648,8 +2761,6 @@
             } else {
                 mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
                 mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
-                setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
-                fixateNewestUserKeyAuth(userId);
                 setKeystorePassword(sp.deriveKeyStorePassword(), userId);
             }
         } else {
@@ -2659,9 +2770,7 @@
 
             mSpManager.clearSidForUser(userId);
             gateKeeperClearSecureUserId(userId);
-            unlockUserKey(userId, sp.deriveFileBasedEncryptionKey());
-            clearUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
-            fixateNewestUserKeyAuth(userId);
+            unlockUserKey(userId, sp);
             unlockKeystore(sp.deriveKeyStorePassword(), userId);
             setKeystorePassword(null, userId);
             removeBiometricsForUser(userId);
@@ -2804,6 +2913,7 @@
             if (!isUserSecure(userId)) {
                 long protectorId = getCurrentLskfBasedProtectorId(userId);
                 if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+                    // TODO(b/232452368): this case is only needed by unit tests now; remove it.
                     sp = initializeSyntheticPasswordLocked(userId);
                 } else {
                     sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
@@ -2888,7 +2998,7 @@
                 // If clearing credential, unlock the user manually in order to progress user start
                 // Call unlockUser() on a handler thread so no lock is held (either by LSS or by
                 // the caller like DPMS), otherwise it can lead to deadlock.
-                mHandler.post(() -> unlockUser(userId, null));
+                mHandler.post(() -> unlockUser(userId));
             }
             notifyPasswordChanged(credential, userId);
             notifySeparateProfileChallengeChanged(userId);
@@ -3041,6 +3151,9 @@
         pw.decreaseIndent();
 
         pw.println("PasswordHandleCount: " + mGatekeeperPasswords.size());
+        synchronized (mUserCreationAndRemovalLock) {
+            pw.println("BootComplete: " + mBootComplete);
+        }
     }
 
     private void dumpKeystoreKeys(IndentingPrintWriter pw) {
@@ -3197,6 +3310,21 @@
     private final class LocalService extends LockSettingsInternal {
 
         @Override
+        public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+            LockSettingsService.this.unlockUserKeyIfUnsecured(userId);
+        }
+
+        @Override
+        public void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+            LockSettingsService.this.createNewUser(userId, userSerialNumber);
+        }
+
+        @Override
+        public void removeUser(@UserIdInt int userId) {
+            LockSettingsService.this.removeUser(userId);
+        }
+
+        @Override
         public long addEscrowToken(byte[] token, int userId,
                 EscrowTokenStateChangeCallback callback) {
             return LockSettingsService.this.addEscrowToken(token, TOKEN_TYPE_STRONG, userId,
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index f1afb96..66bdadb 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -49,6 +49,7 @@
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.VerifyCredentialResponse;
 import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+import com.android.server.utils.Slogf;
 
 import libcore.util.HexEncoding;
 
@@ -79,11 +80,12 @@
  *    LockscreenCredential.  The LSKF may be empty (none).  There may be escrow token-based
  *    protectors as well, only for specific use cases such as enterprise-managed users.
  *
- *  - While the user's LSKF is nonempty, the SP protects the user's CE (credential encrypted)
- *    storage and auth-bound Keystore keys: the user's CE key is encrypted by an SP-derived secret,
- *    and the user's Keystore and Gatekeeper passwords are other SP-derived secrets.  However, while
- *    the user's LSKF is empty, these protections are cleared; this is needed to invalidate the
- *    auth-bound keys and make UserController.unlockUser() work with an empty secret.
+ *  - The user's credential-encrypted storage is always protected by the SP.
+ *
+ *  - The user's auth-bound Keystore keys are protected by the SP, but only while an LSKF is set.
+ *    This works by setting the user's Keystore and Gatekeeper passwords to SP-derived secrets, but
+ *    only while an LSKF is set.  When the LSKF is removed, these passwords are cleared,
+ *    invalidating the user's auth-bound keys.
  *
  * Files stored on disk for each user:
  *   For the SP itself, stored under NULL_PROTECTOR_ID:
@@ -1026,6 +1028,14 @@
             long protectorId, @NonNull LockscreenCredential credential, int userId,
             ICheckCredentialProgressCallback progressCallback) {
         AuthenticationResult result = new AuthenticationResult();
+
+        if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+            // This should never happen, due to the migration done in LSS.bootCompleted().
+            Slogf.wtf(TAG, "Synthetic password not found for user %d", userId);
+            result.gkResponse = VerifyCredentialResponse.ERROR;
+            return result;
+        }
+
         PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
                     userId));
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0c601bf..890c891 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1962,10 +1962,15 @@
 
                             continue;
                         case TAG_SHORTCUT:
-                            final ShortcutInfo si = parseShortcut(parser, packageName,
-                                    shortcutUser.getUserId(), fromBackup);
-                            // Don't use addShortcut(), we don't need to save the icon.
-                            ret.mShortcuts.put(si.getId(), si);
+                            try {
+                                final ShortcutInfo si = parseShortcut(parser, packageName,
+                                        shortcutUser.getUserId(), fromBackup);
+                                // Don't use addShortcut(), we don't need to save the icon.
+                                ret.mShortcuts.put(si.getId(), si);
+                            } catch (Exception e) {
+                                // b/246540168 malformed shortcuts should be ignored
+                                Slog.e(TAG, "Failed parsing shortcut.", e);
+                            }
                             continue;
                         case TAG_SHARE_TARGET:
                             ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser));
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c39cbae..0a89d13 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -88,8 +88,6 @@
 import android.os.storage.StorageManager;
 import android.os.storage.StorageManagerInternal;
 import android.provider.Settings;
-import android.security.GateKeeper;
-import android.service.gatekeeper.IGateKeeperService;
 import android.service.voice.VoiceInteractionManagerInternal;
 import android.stats.devicepolicy.DevicePolicyEnums;
 import android.text.TextUtils;
@@ -4664,6 +4662,10 @@
                     StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
             t.traceEnd();
 
+            t.traceBegin("LSS.createNewUser");
+            mLockPatternUtils.createNewUser(userId, userInfo.serialNumber);
+            t.traceEnd();
+
             final Set<String> userTypeInstallablePackages =
                     mSystemPackageInstaller.getInstallablePackagesForUserType(userType);
             t.traceBegin("PM.createNewUser");
@@ -5500,15 +5502,8 @@
             Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
         }
 
-        // Cleanup gatekeeper secure user id
-        try {
-            final IGateKeeperService gk = GateKeeper.getService();
-            if (gk != null) {
-                gk.clearSecureUserId(userId);
-            }
-        } catch (Exception ex) {
-            Slog.w(LOG_TAG, "unable to clear GK secure user id");
-        }
+        // Cleanup lock settings
+        mLockPatternUtils.removeUser(userId);
 
         // Cleanup package manager settings
         mPm.cleanUpUser(this, userId);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 2d22b8f..f9352cb 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -5316,7 +5316,7 @@
     private final class SuspendBlockerImpl implements SuspendBlocker {
         private static final String UNKNOWN_ID = "unknown";
         private final String mName;
-        private final String mTraceName;
+        private final int mNameHash;
         private int mReferenceCount;
 
         // Maps suspend blocker IDs to a list (LongArray) of open acquisitions for the suspend
@@ -5325,7 +5325,7 @@
 
         public SuspendBlockerImpl(String name) {
             mName = name;
-            mTraceName = "SuspendBlocker (" + name + ")";
+            mNameHash = mName.hashCode();
         }
 
         @Override
@@ -5336,7 +5336,8 @@
                             + "\" was finalized without being released!");
                     mReferenceCount = 0;
                     mNativeWrapper.nativeReleaseSuspendBlocker(mName);
-                    Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+                    Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+                            "SuspendBlockers", mNameHash);
                 }
             } finally {
                 super.finalize();
@@ -5357,7 +5358,8 @@
                     if (DEBUG_SPEW) {
                         Slog.d(TAG, "Acquiring suspend blocker \"" + mName + "\".");
                     }
-                    Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0);
+                    Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER,
+                            "SuspendBlockers", mName, mNameHash);
                     mNativeWrapper.nativeAcquireSuspendBlocker(mName);
                 }
             }
@@ -5378,7 +5380,10 @@
                         Slog.d(TAG, "Releasing suspend blocker \"" + mName + "\".");
                     }
                     mNativeWrapper.nativeReleaseSuspendBlocker(mName);
-                    Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+                    if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
+                        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+                                "SuspendBlockers", mNameHash);
+                    }
                 } else if (mReferenceCount < 0) {
                     Slog.wtf(TAG, "Suspend blocker \"" + mName
                             + "\" was released without being acquired!", new Throwable());
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index c6128f9..8a84860 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -6813,20 +6813,31 @@
         synchronized (mModemNetworkLock) {
             if (displayTransport == TRANSPORT_CELLULAR) {
                 mModemIfaces = includeInStringArray(mModemIfaces, iface);
-                if (DEBUG) Slog.d(TAG, "Note mobile iface " + iface + ": " + mModemIfaces);
+                if (DEBUG) {
+                    Slog.d(TAG, "Note mobile iface " + iface + ": "
+                            + Arrays.toString(mModemIfaces));
+                }
             } else {
                 mModemIfaces = excludeFromStringArray(mModemIfaces, iface);
-                if (DEBUG) Slog.d(TAG, "Note non-mobile iface " + iface + ": " + mModemIfaces);
+                if (DEBUG) {
+                    Slog.d(TAG, "Note non-mobile iface " + iface + ": "
+                            + Arrays.toString(mModemIfaces));
+                }
             }
         }
 
         synchronized (mWifiNetworkLock) {
             if (displayTransport == TRANSPORT_WIFI) {
                 mWifiIfaces = includeInStringArray(mWifiIfaces, iface);
-                if (DEBUG) Slog.d(TAG, "Note wifi iface " + iface + ": " + mWifiIfaces);
+                if (DEBUG) {
+                    Slog.d(TAG, "Note wifi iface " + iface + ": " + Arrays.toString(mWifiIfaces));
+                }
             } else {
                 mWifiIfaces = excludeFromStringArray(mWifiIfaces, iface);
-                if (DEBUG) Slog.d(TAG, "Note non-wifi iface " + iface + ": " + mWifiIfaces);
+                if (DEBUG) {
+                    Slog.d(TAG, "Note non-wifi iface " + iface + ": "
+                            + Arrays.toString(mWifiIfaces));
+                }
             }
         }
     }
@@ -12622,8 +12633,7 @@
     private void updateCpuMeasuredEnergyStatsLocked(@NonNull long[] clusterChargeUC,
             @NonNull CpuDeltaPowerAccumulator accumulator) {
         if (DEBUG_ENERGY) {
-            Slog.d(TAG,
-                    "Updating cpu cluster stats: " + clusterChargeUC.toString());
+            Slog.d(TAG, "Updating cpu cluster stats: " + Arrays.toString(clusterChargeUC));
         }
         if (mGlobalMeasuredEnergyStats == null) {
             return;
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index fd6c974..b160af6a 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -98,7 +98,7 @@
                     throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
                 }
                 return mService.getRecentTasks().createRecentTaskInfo(task,
-                        false /* stripExtras */);
+                        false /* stripExtras */, true /* getTasksAllowed */);
             } finally {
                 Binder.restoreCallingIdentity(origId);
             }
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 4860762..1fc061b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -976,7 +976,7 @@
                 continue;
             }
 
-            res.add(createRecentTaskInfo(task, true /* stripExtras */));
+            res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed));
         }
         return res;
     }
@@ -1895,7 +1895,8 @@
     /**
      * Creates a new RecentTaskInfo from a Task.
      */
-    ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) {
+    ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras,
+            boolean getTasksAllowed) {
         final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
         // If the recent Task is detached, we consider it will be re-attached to the default
         // TaskDisplayArea because we currently only support recent overview in the default TDA.
@@ -1907,6 +1908,9 @@
         rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID;
         rti.persistentId = rti.taskId;
         rti.lastSnapshotData.set(tr.mLastTaskSnapshotData);
+        if (!getTasksAllowed) {
+            Task.trimIneffectiveInfo(tr, rti);
+        }
 
         // Fill in organized child task info for the task created by organizer.
         if (tr.mCreatedByOrganizer) {
diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java
index a753b55..9c85bc0 100644
--- a/services/core/java/com/android/server/wm/RunningTasks.java
+++ b/services/core/java/com/android/server/wm/RunningTasks.java
@@ -138,6 +138,10 @@
         task.fillTaskInfo(rti, !mKeepIntentExtra);
         // Fill in some deprecated values
         rti.id = rti.taskId;
+
+        if (!mAllowed) {
+            Task.trimIneffectiveInfo(task, rti);
+        }
         return rti;
     }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 62d93ad..5bd141a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3431,6 +3431,27 @@
         info.isSleeping = shouldSleepActivities();
     }
 
+    /**
+     * Removes the activity info if the activity belongs to a different uid, which is
+     * different from the app that hosts the task.
+     */
+    static void trimIneffectiveInfo(Task task, TaskInfo info) {
+        final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing,
+                false /* traverseTopToBottom */);
+        final int baseActivityUid =
+                baseActivity != null ? baseActivity.getUid() : task.effectiveUid;
+
+        if (info.topActivityInfo != null
+                && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) {
+            info.topActivity = null;
+            info.topActivityInfo = null;
+        }
+
+        if (task.effectiveUid != baseActivityUid) {
+            info.baseActivity = null;
+        }
+    }
+
     @Nullable PictureInPictureParams getPictureInPictureParams() {
         final Task topTask = getTopMostTask();
         if (topTask == null) return null;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 1497a19..502b4bb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6012,7 +6012,11 @@
         if (mFrozenDisplayId != INVALID_DISPLAY && mFrozenDisplayId == w.getDisplayId()
                 && mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_TIMEOUT) {
             ProtoLog.v(WM_DEBUG_ORIENTATION, "Changing surface while display frozen: %s", w);
-            w.setOrientationChanging(true);
+            // WindowsState#reportResized won't tell invisible requested window to redraw,
+            // so do not set it as changing orientation to avoid affecting draw state.
+            if (w.isVisibleRequested()) {
+                w.setOrientationChanging(true);
+            }
             if (mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_NONE) {
                 mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_ACTIVE;
                 // XXX should probably keep timeout from
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index e2f833c..2304ab4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -46,6 +46,7 @@
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
 import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED;
+import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
@@ -240,8 +241,18 @@
     }
 
     @Override
-    public IBinder startTransition(int type, @Nullable IBinder transitionToken,
+    public IBinder startNewTransition(int type, @Nullable WindowContainerTransaction t) {
+        return startTransition(type, null /* transitionToken */, t);
+    }
+
+    @Override
+    public void startTransition(@NonNull IBinder transitionToken,
             @Nullable WindowContainerTransaction t) {
+        startTransition(-1 /* unused type */, transitionToken, t);
+    }
+
+    private IBinder startTransition(@WindowManager.TransitionType int type,
+            @Nullable IBinder transitionToken, @Nullable WindowContainerTransaction t) {
         enforceTaskPermission("startTransition()");
         final CallerInfo caller = new CallerInfo();
         final long ident = Binder.clearCallingIdentity();
@@ -1549,10 +1560,6 @@
         return (cfgChanges & CONTROLLABLE_CONFIGS) == 0;
     }
 
-    private void enforceTaskPermission(String func) {
-        mService.enforceTaskPermission(func);
-    }
-
     private boolean isValidTransaction(@NonNull WindowContainerTransaction t) {
         if (t.getTaskFragmentOrganizer() != null && !mTaskFragmentOrganizerController
                 .isOrganizerRegistered(t.getTaskFragmentOrganizer())) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 68fabc5..0f5184a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3868,7 +3868,7 @@
         // configuration update when the window has requested to be hidden. Doing so can lead to
         // the client erroneously accepting a configuration that would have otherwise caused an
         // activity restart. We instead hand back the last reported {@link MergedConfiguration}.
-        if (useLatestConfig || (relayoutVisible && (shouldCheckTokenVisibleRequested()
+        if (useLatestConfig || (relayoutVisible && (!shouldCheckTokenVisibleRequested()
                 || mToken.isVisibleRequested()))) {
             final Configuration globalConfig = getProcessGlobalConfiguration();
             final Configuration overrideConfig = getMergedOverrideConfiguration();
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 9abf107..2584b86 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -12,6 +12,7 @@
 per-file com_android_server_am_BatteryStatsService.cpp = file:/BATTERY_STATS_OWNERS
 
 per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
+per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
 per-file com_android_server_Usb* = file:/services/usb/OWNERS
 per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
 per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 1d56078..d3d69ae 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -491,10 +491,10 @@
     compactProcessOrFallback(pid, compactionFlags);
 }
 
-static jint com_android_server_am_CachedAppOptimizer_freezeBinder(
-        JNIEnv *env, jobject clazz, jint pid, jboolean freeze) {
-
-    jint retVal = IPCThreadState::freeze(pid, freeze, 100 /* timeout [ms] */);
+static jint com_android_server_am_CachedAppOptimizer_freezeBinder(JNIEnv* env, jobject clazz,
+                                                                  jint pid, jboolean freeze,
+                                                                  jint timeout_ms) {
+    jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
     if (retVal != 0 && retVal != -EAGAIN) {
         jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
     }
@@ -548,7 +548,7 @@
          (void*)com_android_server_am_CachedAppOptimizer_getMemoryFreedCompaction},
         {"compactSystem", "()V", (void*)com_android_server_am_CachedAppOptimizer_compactSystem},
         {"compactProcess", "(II)V", (void*)com_android_server_am_CachedAppOptimizer_compactProcess},
-        {"freezeBinder", "(IZ)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
+        {"freezeBinder", "(IZI)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
         {"getBinderFreezeInfo", "(I)I",
          (void*)com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo},
         {"getFreezerCheckPath", "()Ljava/lang/String;",
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 267cff6..98e5f1d 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -351,6 +351,35 @@
             <xs:annotation name="nonnull"/>
             <xs:annotation name="final"/>
         </xs:element>
+        <xs:sequence>
+        <!--  Thresholds as tenths of percent of current brightness level, at each level of
+            brightness -->
+            <xs:element name="brightnessThresholdPoints" type="thresholdPoints" maxOccurs="1" minOccurs="0">
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="thresholdPoints">
+        <xs:sequence>
+            <xs:element type="thresholdPoint" name="brightnessThresholdPoint" maxOccurs="unbounded" minOccurs="1">
+                <xs:annotation name="nonnull"/>
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="thresholdPoint">
+        <xs:sequence>
+            <xs:element type="nonNegativeDecimal" name="threshold">
+                <xs:annotation name="nonnull"/>
+                <xs:annotation name="final"/>
+            </xs:element>
+            <xs:element type="nonNegativeDecimal" name="percentage">
+                <xs:annotation name="nonnull"/>
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
     </xs:complexType>
 
     <xs:complexType name="autoBrightness">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index f8bff75..748ef4b 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -13,7 +13,9 @@
 
   public class BrightnessThresholds {
     ctor public BrightnessThresholds();
+    method public final com.android.server.display.config.ThresholdPoints getBrightnessThresholdPoints();
     method @NonNull public final java.math.BigDecimal getMinimum();
+    method public final void setBrightnessThresholdPoints(com.android.server.display.config.ThresholdPoints);
     method public final void setMinimum(@NonNull java.math.BigDecimal);
   }
 
@@ -204,6 +206,19 @@
     method public final void setBrightnessThrottlingMap(@NonNull com.android.server.display.config.BrightnessThrottlingMap);
   }
 
+  public class ThresholdPoint {
+    ctor public ThresholdPoint();
+    method @NonNull public final java.math.BigDecimal getPercentage();
+    method @NonNull public final java.math.BigDecimal getThreshold();
+    method public final void setPercentage(@NonNull java.math.BigDecimal);
+    method public final void setThreshold(@NonNull java.math.BigDecimal);
+  }
+
+  public class ThresholdPoints {
+    ctor public ThresholdPoints();
+    method @NonNull public final java.util.List<com.android.server.display.config.ThresholdPoint> getBrightnessThresholdPoint();
+  }
+
   public class Thresholds {
     ctor public Thresholds();
     method @NonNull public final com.android.server.display.config.BrightnessThresholds getBrighteningThresholds();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 972d4e8..a561307 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -7709,6 +7709,7 @@
         Preconditions.checkCallAuthorization(
                 isProfileOwner(caller) || isDefaultDeviceOwner(caller)
                         || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY));
+        Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId));
 
         synchronized (getLockObject()) {
             if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) {
@@ -7749,6 +7750,7 @@
         Preconditions.checkCallAuthorization(
                 isProfileOwner(caller) || isDefaultDeviceOwner(caller)
                         || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY));
+        Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId));
 
         synchronized (getLockObject()) {
             if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index d8f282a..9e449ae 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -331,6 +331,8 @@
             "com.android.server.wallpaper.WallpaperManagerService$Lifecycle";
     private static final String AUTO_FILL_MANAGER_SERVICE_CLASS =
             "com.android.server.autofill.AutofillManagerService";
+    private static final String CREDENTIAL_MANAGER_SERVICE_CLASS =
+            "com.android.server.credentials.CredentialManagerService";
     private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS =
             "com.android.server.contentcapture.ContentCaptureManagerService";
     private static final String TRANSLATION_MANAGER_SERVICE_CLASS =
@@ -2571,6 +2573,12 @@
             t.traceEnd();
         }
 
+        if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_CREDENTIALS)) {
+            t.traceBegin("StartCredentialManagerService");
+            mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
+            t.traceEnd();
+        }
+
         // Translation manager service
         if (deviceHasConfigString(context, R.string.config_defaultTranslationService)) {
             t.traceBegin("StartTranslationManagerService");
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 86d6169..f89d378 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -364,7 +364,7 @@
             mActiveProcesses.remove(r);
             mRegisteredReceivers.remove(r.getPid());
             return invocation.callRealMethod();
-        }).when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean());
+        }).when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(), anyBoolean());
 
         // If we're entirely dead, rely on default behaviors above
         if (dead) return r;
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 7ae70eb..6551bde 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -113,7 +113,7 @@
     <uses-sdk android:minSdkVersion="1"
          android:targetSdkVersion="26"/>
 
-    <application android:testOnly="true">
+    <application android:testOnly="true" android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
 
         <service android:name="com.android.server.accounts.TestAccountType1AuthenticatorService"
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 96c3823..2d2c76c 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -94,6 +94,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.widget.LockPatternUtils;
 import com.android.server.FgThread;
 import com.android.server.SystemService;
 import com.android.server.am.UserState.KeyEvictedCallback;
@@ -834,8 +835,7 @@
     private void setUpAndStartUserInBackground(int userId) throws Exception {
         setUpUser(userId, 0);
         mUserController.startUser(userId, /* foreground= */ false);
-        verify(mInjector.mStorageManagerMock, times(1))
-                .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null);
+        verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId);
         mUserStates.put(userId, mUserController.getStartedUserState(userId));
     }
 
@@ -843,8 +843,7 @@
         setUpUser(userId, UserInfo.FLAG_PROFILE, false, UserManager.USER_TYPE_PROFILE_MANAGED);
         assertThat(mUserController.startProfile(userId)).isTrue();
 
-        verify(mInjector.mStorageManagerMock, times(1))
-                .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null);
+        verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId);
         mUserStates.put(userId, mUserController.getStartedUserState(userId));
     }
 
@@ -966,6 +965,7 @@
         private final UserManagerInternal mUserManagerInternalMock;
         private final WindowManagerService mWindowManagerMock;
         private final KeyguardManager mKeyguardManagerMock;
+        private final LockPatternUtils mLockPatternUtilsMock;
 
         private final Context mCtx;
 
@@ -982,6 +982,7 @@
             mStorageManagerMock = mock(IStorageManager.class);
             mKeyguardManagerMock = mock(KeyguardManager.class);
             when(mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
+            mLockPatternUtilsMock = mock(LockPatternUtils.class);
         }
 
         @Override
@@ -1081,6 +1082,11 @@
         protected void dismissKeyguard(Runnable runnable, String reason) {
             runnable.run();
         }
+
+        @Override
+        protected LockPatternUtils getLockPatternUtils() {
+            return mLockPatternUtilsMock;
+        }
     }
 
     private static class TestHandler extends Handler {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 41f7433..12b8264 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -39,7 +39,6 @@
 
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.HalClientMonitor;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 
@@ -64,8 +63,6 @@
     private IFace mDaemon;
     @Mock
     private BiometricContext mBiometricContext;
-    @Mock
-    private BiometricStateCallback mBiometricStateCallback;
 
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -94,8 +91,8 @@
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
-        mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback,
-                mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext);
+        mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG,
+                mLockoutResetDispatcher, mBiometricContext);
     }
 
     @SuppressWarnings("rawtypes")
@@ -143,13 +140,11 @@
 
         TestableFaceProvider(@NonNull IFace daemon,
                 @NonNull Context context,
-                @NonNull BiometricStateCallback biometricStateCallback,
                 @NonNull SensorProps[] props,
                 @NonNull String halInstanceName,
                 @NonNull LockoutResetDispatcher lockoutResetDispatcher,
                 @NonNull BiometricContext biometricContext) {
-            super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                    biometricContext);
+            super(context, props, halInstanceName, lockoutResetDispatcher, biometricContext);
             mDaemon = daemon;
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
index a2cade7..116d2d5 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
@@ -43,7 +43,6 @@
 
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 
 import org.junit.Before;
@@ -74,8 +73,6 @@
     private BiometricScheduler mScheduler;
     @Mock
     private BiometricContext mBiometricContext;
-    @Mock
-    private BiometricStateCallback mBiometricStateCallback;
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -106,8 +103,8 @@
                 resetLockoutRequiresChallenge);
 
         Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST"));
-        mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps,
-                mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext);
+        mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mHandler, mScheduler,
+                mBiometricContext);
         mBinder = new Binder();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 57ded99..6388c7d 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -236,10 +236,9 @@
                 .setBlockedActivities(getBlockedActivities())
                 .build();
         mDeviceImpl = new VirtualDeviceImpl(mContext,
-                mAssociationInfo, new Binder(), /* uid */ 0, mInputController,
-                (int associationId) -> {
-                }, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback,
-                params);
+                mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
+                mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
+                mActivityListener, mRunningAppsChangedCallback, params);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index fc2a4cf..0206839 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -420,13 +420,13 @@
 
     @Test
     public void testHysteresisLevels() {
-        int[] ambientBrighteningThresholds = {100, 200};
-        int[] ambientDarkeningThresholds = {400, 500};
-        int[] ambientThresholdLevels = {500};
+        float[] ambientBrighteningThresholds = {50, 100};
+        float[] ambientDarkeningThresholds = {10, 20};
+        float[] ambientThresholdLevels = {0, 500};
         float ambientDarkeningMinChangeThreshold = 3.0f;
         float ambientBrighteningMinChangeThreshold = 1.5f;
         HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds,
-                ambientDarkeningThresholds, ambientThresholdLevels,
+                ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels,
                 ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold);
 
         // test low, activate minimum change thresholds.
@@ -435,16 +435,17 @@
         assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON);
 
         // test max
-        assertEquals(12000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON);
-        assertEquals(5000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
+        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
+        assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2);
+        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
 
         // test just below threshold
-        assertEquals(548.9f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
-        assertEquals(299.4f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
+        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
+        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
 
         // test at (considered above) threshold
-        assertEquals(600f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
-        assertEquals(250f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
+        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
+        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 04702c4..a6a2419 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -31,6 +31,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.R;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,6 +48,8 @@
 @RunWith(AndroidJUnit4.class)
 public final class DisplayDeviceConfigTest {
     private DisplayDeviceConfig mDisplayDeviceConfig;
+    private static final float ZERO_DELTA = 0.0f;
+    private static final float SMALL_DELTA = 0.0001f;
     @Mock
     private Context mContext;
 
@@ -67,26 +71,74 @@
         assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
-        assertEquals(mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), 10.0f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), 2.0f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, 0.0f);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getBrightness(), new float[]{0.0f, 0.62f, 1.0f},
-                0.0f);
-        assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f}, 0.0f);
+                ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f},
+                ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getBacklight(), new float[]{0.0f, 0.62f, 1.0f},
-                0.0f);
-        assertEquals(mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), 0.001, 0.000001f);
-        assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f);
+                ZERO_DELTA);
         assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000);
         assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 50.0f, 80.0f}, 0.0f);
+                float[]{0.0f, 50.0f, 80.0f}, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{45.32f, 75.43f}, 0.0f);
+                float[]{45.32f, 75.43f}, ZERO_DELTA);
+
+        // Test thresholds
+        assertEquals(10, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(),
+                ZERO_DELTA);
+        assertEquals(20, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+                ZERO_DELTA);
+        assertEquals(30, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(40, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertEquals(0.1f, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0.2f, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+        assertEquals(0.3f, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0.4f, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 0.10f, 0.20f},
+                mDisplayDeviceConfig.getScreenBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{9, 10, 11},
+                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 0.11f, 0.21f},
+                mDisplayDeviceConfig.getScreenDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{11, 12, 13},
+                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 100, 200},
+                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{13, 14, 15},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 300, 400},
+                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{15, 16, 17},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 0.12f, 0.22f},
+                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{17, 18, 19},
+                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 0.13f, 0.23f},
+                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{19, 20, 21},
+                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 500, 600},
+                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{21, 22, 23},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 700, 800},
+                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{23, 24, 25},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
+
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
     }
@@ -95,9 +147,61 @@
     public void testConfigValuesFromConfigResource() {
         setupDisplayDeviceConfigFromConfigResourceFile();
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{2.0f, 200.0f, 600.0f}, 0.0f);
+                float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 0.0f, 110.0f, 500.0f}, 0.0f);
+                float[]{0.0f, 0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+
+        // Test thresholds
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+                ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        // screen levels will be considered "old screen brightness scale"
+        // and therefore will divide by 255
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenBrighteningLevels(), SMALL_DELTA);
+        assertArrayEquals(new float[]{35, 36, 37},
+                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenDarkeningLevels(), SMALL_DELTA);
+        assertArrayEquals(new float[]{37, 38, 39},
+                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{27, 28, 29},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{29, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), SMALL_DELTA);
+        assertArrayEquals(new float[]{35, 36, 37},
+                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), SMALL_DELTA);
+        assertArrayEquals(new float[]{37, 38, 39},
+                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{27, 28, 29},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{29, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
+
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
     }
@@ -152,11 +256,126 @@
                 +   "<ambientBrightnessChangeThresholds>\n"
                 +       "<brighteningThresholds>\n"
                 +           "<minimum>10</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>13</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>100</threshold><percentage>14</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>200</threshold><percentage>15</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
                 +       "</brighteningThresholds>\n"
                 +       "<darkeningThresholds>\n"
-                +           "<minimum>2</minimum>\n"
+                +           "<minimum>30</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>15</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>300</threshold><percentage>16</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>400</threshold><percentage>17</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
                 +       "</darkeningThresholds>\n"
                 +   "</ambientBrightnessChangeThresholds>\n"
+                +   "<displayBrightnessChangeThresholds>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>0.1</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold>\n"
+                +                   "<percentage>9</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.10</threshold>\n"
+                +                   "<percentage>10</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.20</threshold>\n"
+                +                   "<percentage>11</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>0.3</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>11</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.11</threshold><percentage>12</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.21</threshold><percentage>13</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</displayBrightnessChangeThresholds>\n"
+                +   "<ambientBrightnessChangeThresholdsIdle>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>20</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>21</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>500</threshold><percentage>22</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>600</threshold><percentage>23</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>40</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>23</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>700</threshold><percentage>24</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>800</threshold><percentage>25</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</ambientBrightnessChangeThresholdsIdle>\n"
+                +   "<displayBrightnessChangeThresholdsIdle>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>0.2</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>17</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.12</threshold><percentage>18</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.22</threshold><percentage>19</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>0.4</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>19</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.13</threshold><percentage>20</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.23</threshold><percentage>21</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</displayBrightnessChangeThresholdsIdle>\n"
                 +   "<screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> "
                 +   "<screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease>  "
                 +   "<screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>"
@@ -169,18 +388,6 @@
                 +   "</screenBrightnessRampDecreaseMaxMillis>"
                 +   "<ambientLightHorizonLong>5000</ambientLightHorizonLong>\n"
                 +   "<ambientLightHorizonShort>50</ambientLightHorizonShort>\n"
-                +   "<displayBrightnessChangeThresholds>"
-                +       "<brighteningThresholds>"
-                +           "<minimum>"
-                +               "0.001"
-                +           "</minimum>"
-                +       "</brighteningThresholds>"
-                +       "<darkeningThresholds>"
-                +           "<minimum>"
-                +               "0.002"
-                +           "</minimum>"
-                +       "</darkeningThresholds>"
-                +   "</displayBrightnessChangeThresholds>"
                 +   "<screenBrightnessRampIncreaseMaxMillis>"
                 +       "2000"
                 +    "</screenBrightnessRampIncreaseMaxMillis>\n"
@@ -239,8 +446,24 @@
                 com.android.internal.R.array.config_autoBrightnessLevels))
                 .thenReturn(screenBrightnessLevelLux);
 
-        mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
+        // Thresholds
+        // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
+        // of length N+1
+        when(mResources.getIntArray(com.android.internal.R.array.config_ambientThresholdLevels))
+                .thenReturn(new int[]{30, 31});
+        when(mResources.getIntArray(com.android.internal.R.array.config_screenThresholdLevels))
+                .thenReturn(new int[]{42, 43});
+        when(mResources.getIntArray(
+                com.android.internal.R.array.config_ambientBrighteningThresholds))
+                .thenReturn(new int[]{270, 280, 290});
+        when(mResources.getIntArray(com.android.internal.R.array.config_ambientDarkeningThresholds))
+                .thenReturn(new int[]{290, 300, 310});
+        when(mResources.getIntArray(R.array.config_screenBrighteningThresholds))
+                .thenReturn(new int[]{350, 360, 370});
+        when(mResources.getIntArray(R.array.config_screenDarkeningThresholds))
+                .thenReturn(new int[]{370, 380, 390});
 
+        mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
     }
 
     private TypedArray createFloatTypedArray(float[] vals) {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index e220841..c934e65 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -168,16 +168,15 @@
         allUsers.add(SECONDARY_USER_INFO);
         when(mUserManager.getUsers()).thenReturn(allUsers);
 
-        when(mActivityManager.unlockUser(anyInt(), any(), any(), any())).thenAnswer(
-                new Answer<Boolean>() {
-            @Override
-            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+        when(mActivityManager.unlockUser2(anyInt(), any())).thenAnswer(
+            invocation -> {
                 Object[] args = invocation.getArguments();
-                mStorageManager.unlockUser((int)args[0], (byte[])args[2],
-                        (IProgressListener) args[3]);
+                int userId = (int) args[0];
+                IProgressListener listener = (IProgressListener) args[1];
+                listener.onStarted(userId, null);
+                listener.onFinished(userId, null);
                 return true;
-            }
-        });
+            });
 
         // Adding a fake Device Owner app which will enable escrow token support in LSS.
         when(mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser()).thenReturn(
@@ -215,37 +214,20 @@
     private IStorageManager setUpStorageManagerMock() throws RemoteException {
         final IStorageManager sm = mock(IStorageManager.class);
 
-        doAnswer(new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocation) throws Throwable {
-                Object[] args = invocation.getArguments();
-                mStorageManager.addUserKeyAuth((int) args[0] /* userId */,
-                        (int) args[1] /* serialNumber */,
-                        (byte[]) args[2] /* secret */);
-                return null;
-            }
-        }).when(sm).addUserKeyAuth(anyInt(), anyInt(), any());
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            mStorageManager.unlockUserKey(/* userId= */ (int) args[0],
+                    /* secret= */ (byte[]) args[2]);
+            return null;
+        }).when(sm).unlockUserKey(anyInt(), anyInt(), any());
 
-        doAnswer(new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocation) throws Throwable {
-                Object[] args = invocation.getArguments();
-                mStorageManager.clearUserKeyAuth((int) args[0] /* userId */,
-                        (int) args[1] /* serialNumber */,
-                        (byte[]) args[2] /* secret */);
-                return null;
-            }
-        }).when(sm).clearUserKeyAuth(anyInt(), anyInt(), any());
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            mStorageManager.setUserKeyProtection(/* userId= */ (int) args[0],
+                    /* secret= */ (byte[]) args[1]);
+            return null;
+        }).when(sm).setUserKeyProtection(anyInt(), any());
 
-        doAnswer(
-                new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocation) throws Throwable {
-                Object[] args = invocation.getArguments();
-                mStorageManager.fixateNewestUserKeyAuth((int) args[0] /* userId */);
-                return null;
-            }
-        }).when(sm).fixateNewestUserKeyAuth(anyInt());
         return sm;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
index 619ef70..91f3fed 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
@@ -16,75 +16,26 @@
 
 package com.android.server.locksettings;
 
-import android.os.IProgressListener;
-import android.os.RemoteException;
+import static com.google.common.truth.Truth.assertThat;
+
 import android.util.ArrayMap;
 
-
-import junit.framework.AssertionFailedError;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-
 public class FakeStorageManager {
 
-    private ArrayMap<Integer, ArrayList<byte[]>> mAuth = new ArrayMap<>();
-    private boolean mIgnoreBadUnlock;
+    private final ArrayMap<Integer, byte[]> mUserSecrets = new ArrayMap<>();
 
-    public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-        getUserAuth(userId).add(secret);
-    }
-
-    public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-        ArrayList<byte[]> auths = getUserAuth(userId);
-        if (secret == null) {
-            return;
-        }
-        auths.remove(secret);
-        auths.add(null);
-    }
-
-    public void fixateNewestUserKeyAuth(int userId) {
-        ArrayList<byte[]> auths = mAuth.get(userId);
-        byte[] latest = auths.get(auths.size() - 1);
-        auths.clear();
-        auths.add(latest);
-    }
-
-    private ArrayList<byte[]> getUserAuth(int userId) {
-        if (!mAuth.containsKey(userId)) {
-            ArrayList<byte[]> auths = new ArrayList<>();
-            auths.add(null);
-            mAuth.put(userId, auths);
-        }
-        return mAuth.get(userId);
+    public void setUserKeyProtection(int userId, byte[] secret) {
+        assertThat(mUserSecrets).doesNotContainKey(userId);
+        mUserSecrets.put(userId, secret);
     }
 
     public byte[] getUserUnlockToken(int userId) {
-        ArrayList<byte[]> auths = getUserAuth(userId);
-        if (auths.size() != 1) {
-            throw new AssertionFailedError("More than one secret exists");
-        }
-        return auths.get(0);
+        byte[] secret = mUserSecrets.get(userId);
+        assertThat(secret).isNotNull();
+        return secret;
     }
 
-    public void unlockUser(int userId, byte[] secret, IProgressListener listener)
-            throws RemoteException {
-        listener.onStarted(userId, null);
-        listener.onFinished(userId, null);
-        ArrayList<byte[]> auths = getUserAuth(userId);
-        if (auths.size() > 1) {
-            throw new AssertionFailedError("More than one secret exists");
-        }
-        byte[] auth = auths.get(0);
-        if (!Arrays.equals(secret, auth)) {
-            if (!mIgnoreBadUnlock) {
-                throw new AssertionFailedError("Invalid secret to unlock user " + userId);
-            }
-        }
-    }
-
-    public void setIgnoreBadUnlock(boolean ignore) {
-        mIgnoreBadUnlock = ignore;
+    public void unlockUserKey(int userId, byte[] secret) {
+        assertThat(mUserSecrets.get(userId)).isEqualTo(secret);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 3477288..3f259e3 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -145,15 +145,9 @@
         // Verify that profile which aren't running (e.g. turn off work) don't get unlocked
         assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
 
-        /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
-         * credential as part of verifyCredential() before the new credential is committed in
-         * StorageManager. So we relax the check in our mock StorageManager to allow that.
-         */
-        mStorageManager.setIgnoreBadUnlock(true);
         // Change primary password and verify that profile SID remains
         assertTrue(mService.setLockCredential(
                 secondUnifiedPassword, firstUnifiedPassword, PRIMARY_USER_ID));
-        mStorageManager.setIgnoreBadUnlock(false);
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
         assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
 
@@ -172,15 +166,9 @@
         assertTrue(mService.setLockCredential(primaryPassword,
                 nonePassword(),
                 PRIMARY_USER_ID));
-        /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
-         * credential as part of verifyCredential() before the new credential is committed in
-         * StorageManager. So we relax the check in our mock StorageManager to allow that.
-         */
-        mStorageManager.setIgnoreBadUnlock(true);
         assertTrue(mService.setLockCredential(profilePassword,
                 nonePassword(),
                 MANAGED_PROFILE_USER_ID));
-        mStorageManager.setIgnoreBadUnlock(false);
 
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -203,11 +191,8 @@
         assertNotNull(mGateKeeperService.getAuthToken(MANAGED_PROFILE_USER_ID));
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
 
-        // Change primary credential and make sure we don't affect profile
-        mStorageManager.setIgnoreBadUnlock(true);
         assertTrue(mService.setLockCredential(
                 newPassword("pwd"), primaryPassword, PRIMARY_USER_ID));
-        mStorageManager.setIgnoreBadUnlock(false);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 profilePassword, MANAGED_PROFILE_USER_ID, 0 /* flags */)
                 .getResponseCode());
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 87beece..5e6cccc 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
@@ -126,6 +127,7 @@
         initializeCredential(password, PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
+        verify(mActivityManager).unlockUser2(eq(PRIMARY_USER_ID), any());
 
         assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, mService.verifyCredential(
                 badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
@@ -187,32 +189,13 @@
     }
 
     @Test
-    public void testNoSyntheticPasswordOrCredentialDoesNotPassAuthSecret() throws RemoteException {
-        mService.onUnlockUser(PRIMARY_USER_ID);
-        flushHandlerTasks();
-        verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
-    }
-
-    @Test
-    public void testCredentialDoesNotPassAuthSecret() throws RemoteException {
-        LockscreenCredential password = newPassword("password");
-        initializeCredential(password, PRIMARY_USER_ID);
-
-        reset(mAuthSecretService);
-        mService.onUnlockUser(PRIMARY_USER_ID);
-        flushHandlerTasks();
-        verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
-    }
-
-    @Test
-    public void testSyntheticPasswordButNoCredentialPassesAuthSecret() throws RemoteException {
+    public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException {
         LockscreenCredential password = newPassword("password");
         initializeCredential(password, PRIMARY_USER_ID);
         mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
 
         reset(mAuthSecretService);
-        mService.onUnlockUser(PRIMARY_USER_ID);
-        flushHandlerTasks();
+        mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
         verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 617a34f..91c2fe0 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -54,6 +54,7 @@
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -83,7 +84,6 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.service.dreams.DreamManagerInternal;
 import android.test.mock.MockContentResolver;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -101,6 +101,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Spy;
 
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -137,8 +138,8 @@
     private PackageManager mPackageManager;
     @Mock
     private IBinder mBinder;
-    @Mock
-    private DreamManagerInternal mDreamManager;
+    @Spy
+    private TestInjector mInjector;
     @Captor
     private ArgumentCaptor<Intent> mOrderedBroadcastIntent;
     @Captor
@@ -207,10 +208,10 @@
         addLocalService(WindowManagerInternal.class, mWindowManager);
         addLocalService(PowerManagerInternal.class, mLocalPowerManager);
         addLocalService(TwilightManager.class, mTwilightManager);
-        addLocalService(DreamManagerInternal.class, mDreamManager);
-        
+
+        mInjector = spy(new TestInjector());
         mUiManagerService = new UiModeManagerService(mContext, /* setupWizardComplete= */ true,
-                mTwilightManager, new TestInjector());
+                mTwilightManager, mInjector);
         try {
             mUiManagerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
         } catch (SecurityException e) {/* ignore for permission denial */}
@@ -1321,84 +1322,53 @@
 
     @Test
     public void dreamWhenDocked() {
-        setScreensaverActivateOnDock(true);
-        setScreensaverEnabled(true);
-
         triggerDockIntent();
         verifyAndSendResultBroadcast();
-        verify(mDreamManager).requestDream();
-    }
-
-    @Test
-    public void noDreamWhenDocked_dreamsDisabled() {
-        setScreensaverActivateOnDock(true);
-        setScreensaverEnabled(false);
-
-        triggerDockIntent();
-        verifyAndSendResultBroadcast();
-        verify(mDreamManager, never()).requestDream();
-    }
-
-    @Test
-    public void noDreamWhenDocked_dreamsWhenDockedDisabled() {
-        setScreensaverActivateOnDock(false);
-        setScreensaverEnabled(true);
-
-        triggerDockIntent();
-        verifyAndSendResultBroadcast();
-        verify(mDreamManager, never()).requestDream();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
     @Test
     public void noDreamWhenDocked_keyguardNotShowing_interactive() {
-        setScreensaverActivateOnDock(true);
-        setScreensaverEnabled(true);
         mUiManagerService.setStartDreamImmediatelyOnDock(false);
         when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false);
         when(mPowerManager.isInteractive()).thenReturn(true);
 
         triggerDockIntent();
         verifyAndSendResultBroadcast();
-        verify(mDreamManager, never()).requestDream();
+        verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext);
     }
 
     @Test
     public void dreamWhenDocked_keyguardShowing_interactive() {
-        setScreensaverActivateOnDock(true);
-        setScreensaverEnabled(true);
         mUiManagerService.setStartDreamImmediatelyOnDock(false);
         when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true);
         when(mPowerManager.isInteractive()).thenReturn(false);
 
         triggerDockIntent();
         verifyAndSendResultBroadcast();
-        verify(mDreamManager).requestDream();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
     @Test
     public void dreamWhenDocked_keyguardNotShowing_notInteractive() {
-        setScreensaverActivateOnDock(true);
-        setScreensaverEnabled(true);
         mUiManagerService.setStartDreamImmediatelyOnDock(false);
         when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false);
         when(mPowerManager.isInteractive()).thenReturn(false);
 
         triggerDockIntent();
         verifyAndSendResultBroadcast();
-        verify(mDreamManager).requestDream();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
     @Test
     public void dreamWhenDocked_keyguardShowing_notInteractive() {
-        setScreensaverActivateOnDock(true);
-        setScreensaverEnabled(true);
         mUiManagerService.setStartDreamImmediatelyOnDock(false);
         when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true);
         when(mPowerManager.isInteractive()).thenReturn(false);
 
         triggerDockIntent();
         verifyAndSendResultBroadcast();
-        verify(mDreamManager).requestDream();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
     private void triggerDockIntent() {
@@ -1435,22 +1405,6 @@
                 mOrderedBroadcastIntent.getValue());
     }
 
-    private void setScreensaverEnabled(boolean enable) {
-        Settings.Secure.putIntForUser(
-                mContentResolver,
-                Settings.Secure.SCREENSAVER_ENABLED,
-                enable ? 1 : 0,
-                UserHandle.USER_CURRENT);
-    }
-
-    private void setScreensaverActivateOnDock(boolean enable) {
-        Settings.Secure.putIntForUser(
-                mContentResolver,
-                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
-                enable ? 1 : 0,
-                UserHandle.USER_CURRENT);
-    }
-
     private void requestAllPossibleProjectionTypes() throws RemoteException {
         for (int i = 0; i < Integer.SIZE; ++i) {
             mService.requestProjection(mBinder, 1 << i, PACKAGE_NAME);
@@ -1467,11 +1421,17 @@
         }
 
         public TestInjector(int callingUid) {
-          this.callingUid = callingUid;
+            this.callingUid = callingUid;
         }
 
+        @Override
         public int getCallingUid() {
             return callingUid;
         }
+
+        @Override
+        public void startDreamWhenDockedIfAppropriate(Context context) {
+            // do nothing
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 170b388..5def703 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -30,6 +30,7 @@
 import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.os.Process.NOBODY_UID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -1220,20 +1221,34 @@
 
     @Test
     public void testCreateRecentTaskInfo_detachedTask() {
-        final Task task = createTaskBuilder(".Task").setCreateActivity(true).build();
+        final Task task = createTaskBuilder(".Task").build();
+        new ActivityBuilder(mSupervisor.mService)
+                .setTask(task)
+                .setUid(NOBODY_UID)
+                .setComponent(getUniqueComponentName())
+                .build();
         final TaskDisplayArea tda = task.getDisplayArea();
 
         assertTrue(task.isAttached());
         assertTrue(task.supportsMultiWindow());
 
-        RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true);
+        RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertTrue(info.supportsMultiWindow);
 
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                false /* getTasksAllowed */);
+
+        assertTrue(info.topActivity == null);
+        assertTrue(info.topActivityInfo == null);
+        assertTrue(info.baseActivity == null);
+
         // The task can be put in split screen even if it is not attached now.
         task.removeImmediately();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true);
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertTrue(info.supportsMultiWindow);
 
@@ -1242,7 +1257,8 @@
         doReturn(false).when(tda).supportsNonResizableMultiWindow();
         doReturn(false).when(task).isResizeable();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true);
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertFalse(info.supportsMultiWindow);
 
@@ -1250,7 +1266,8 @@
         // the device supports it.
         doReturn(true).when(tda).supportsNonResizableMultiWindow();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true);
+        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+                true /* getTasksAllowed */);
 
         assertTrue(info.supportsMultiWindow);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index c714f01..8370691 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -198,14 +198,25 @@
         mWm.mWindowMap.put(win.mClient.asBinder(), win);
         final int w = 100;
         final int h = 200;
+        final ClientWindowFrames outFrames = new ClientWindowFrames();
+        final MergedConfiguration outConfig = new MergedConfiguration();
+        final SurfaceControl outSurfaceControl = new SurfaceControl();
+        final InsetsState outInsetsState = new InsetsState();
+        final InsetsSourceControl[] outControls = new InsetsSourceControl[0];
+        final Bundle outBundle = new Bundle();
         mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0,
-                new ClientWindowFrames(), new MergedConfiguration(), new SurfaceControl(),
-                new InsetsState(), new InsetsSourceControl[0], new Bundle());
+                outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
         // Because the window is already invisible, it doesn't need to apply exiting animation
         // and WMS#tryStartExitingAnimation() will destroy the surface directly.
         assertFalse(win.mAnimatingExit);
         assertFalse(win.mHasSurface);
         assertNull(win.mWinAnimator.mSurfaceController);
+
+        doReturn(mSystemServicesTestRule.mTransaction).when(SurfaceControl::getGlobalTransaction);
+        // Invisible requested activity should not get the last config even if its view is visible.
+        mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.VISIBLE, 0, 0, 0,
+                outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
+        assertEquals(0, outConfig.getMergedConfiguration().densityDpi);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index e8c8054..1636667 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -735,6 +735,15 @@
         assertTrue(mWm.mResizingWindows.contains(startingApp));
         assertTrue(startingApp.isDrawn());
         assertFalse(startingApp.getOrientationChanging());
+
+        // Even if the display is frozen, invisible requested window should not be affected.
+        startingApp.mActivityRecord.mVisibleRequested = false;
+        mWm.startFreezingDisplay(0, 0, mDisplayContent);
+        doReturn(true).when(mWm.mPolicy).isScreenOn();
+        startingApp.getWindowFrames().setInsetsChanged(true);
+        startingApp.updateResizingWindowIfNeeded();
+        assertTrue(startingApp.isDrawn());
+        assertFalse(startingApp.getOrientationChanging());
     }
 
     @SetupWindows(addWindows = W_ABOVE_ACTIVITY)
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 9c9f5db..b6110247 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1746,7 +1746,7 @@
         }
 
         void startTransition() {
-            mOrganizer.startTransition(mLastRequest.getType(), mLastTransit, null);
+            mOrganizer.startTransition(mLastTransit, null);
         }
 
         void onTransactionReady(SurfaceControl.Transaction t) {
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index 7b7c83f..f848c40 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -190,12 +190,11 @@
     }
 
     /**
-     * Returns the userId of the Context object, if called from a system app,
+     * Returns the userId of the current process, if called from a system app,
      * otherwise it returns the caller's userId
-     * @param context The context object passed in by the caller.
-     * @return
+     * @return userId of the caller.
      */
-    private static int getIncomingUserId(Context context) {
+    private static int getIncomingUserId() {
         int contextUserId = UserHandle.myUserId();
         final int callingUid = Binder.getCallingUid();
         if (DEBUG_MULTIUSER) {
@@ -231,7 +230,7 @@
      */
     @UnsupportedAppUsage
     public static Collection<SmsApplicationData> getApplicationCollection(Context context) {
-        return getApplicationCollectionAsUser(context, getIncomingUserId(context));
+        return getApplicationCollectionAsUser(context, getIncomingUserId());
     }
 
     /**
@@ -590,7 +589,7 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static void setDefaultApplication(String packageName, Context context) {
-        setDefaultApplicationAsUser(packageName, context, getIncomingUserId(context));
+        setDefaultApplicationAsUser(packageName, context, getIncomingUserId());
     }
 
     /**
@@ -952,7 +951,7 @@
      */
     @UnsupportedAppUsage
     public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) {
-        return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId(context));
+        return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
     }
 
     /**
@@ -988,7 +987,18 @@
      */
     @UnsupportedAppUsage
     public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) {
-        int userId = getIncomingUserId(context);
+        return getDefaultMmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+    }
+
+    /**
+     * Gets the default MMS application on a given user
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @param userId target user ID.
+     * @return component name of the app and class to deliver MMS messages to.
+     */
+    public static ComponentName getDefaultMmsApplicationAsUser(Context context,
+            boolean updateIfNeeded, int userId) {
         final long token = Binder.clearCallingIdentity();
         try {
             ComponentName component = null;
@@ -1013,7 +1023,19 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static ComponentName getDefaultRespondViaMessageApplication(Context context,
             boolean updateIfNeeded) {
-        int userId = getIncomingUserId(context);
+        return getDefaultRespondViaMessageApplicationAsUser(context, updateIfNeeded,
+                getIncomingUserId());
+    }
+
+    /**
+     * Gets the default Respond Via Message application on a given user
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured
+     * @param userId target user ID.
+     * @return component name of the app and class to direct Respond Via Message intent to
+     */
+    public static ComponentName getDefaultRespondViaMessageApplicationAsUser(Context context,
+            boolean updateIfNeeded, int userId) {
         final long token = Binder.clearCallingIdentity();
         try {
             ComponentName component = null;
@@ -1039,7 +1061,7 @@
      */
     public static ComponentName getDefaultSendToApplication(Context context,
             boolean updateIfNeeded) {
-        int userId = getIncomingUserId(context);
+        int userId = getIncomingUserId();
         final long token = Binder.clearCallingIdentity();
         try {
             ComponentName component = null;
@@ -1064,7 +1086,20 @@
      */
     public static ComponentName getDefaultExternalTelephonyProviderChangedApplication(
             Context context, boolean updateIfNeeded) {
-        int userId = getIncomingUserId(context);
+        return getDefaultExternalTelephonyProviderChangedApplicationAsUser(context, updateIfNeeded,
+                getIncomingUserId());
+    }
+
+    /**
+     * Gets the default application that handles external changes to the SmsProvider and
+     * MmsProvider on a given user.
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured
+     * @param userId target user ID.
+     * @return component name of the app and class to deliver change intents to.
+     */
+    public static ComponentName getDefaultExternalTelephonyProviderChangedApplicationAsUser(
+            Context context, boolean updateIfNeeded, int userId) {
         final long token = Binder.clearCallingIdentity();
         try {
             ComponentName component = null;
@@ -1089,7 +1124,18 @@
      */
     public static ComponentName getDefaultSimFullApplication(
             Context context, boolean updateIfNeeded) {
-        int userId = getIncomingUserId(context);
+        return getDefaultSimFullApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+    }
+
+    /**
+     * Gets the default application that handles sim full event on a given user.
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured
+     * @param userId target user ID.
+     * @return component name of the app and class to deliver change intents to
+     */
+    public static ComponentName getDefaultSimFullApplicationAsUser(Context context,
+            boolean updateIfNeeded, int userId) {
         final long token = Binder.clearCallingIdentity();
         try {
             ComponentName component = null;
@@ -1114,7 +1160,12 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static boolean shouldWriteMessageForPackage(String packageName, Context context) {
-        return !isDefaultSmsApplication(context, packageName);
+        return !shouldWriteMessageForPackageAsUser(packageName, context, getIncomingUserId());
+    }
+
+    public static boolean shouldWriteMessageForPackageAsUser(String packageName, Context context,
+            int userId) {
+        return !isDefaultSmsApplicationAsUser(context, packageName, userId);
     }
 
     /**
@@ -1126,28 +1177,42 @@
      */
     @UnsupportedAppUsage
     public static boolean isDefaultSmsApplication(Context context, String packageName) {
+        return isDefaultSmsApplicationAsUser(context, packageName, getIncomingUserId());
+    }
+
+    /**
+     * Check if a package is default sms app (or equivalent, like bluetooth) on a given user.
+     *
+     * @param context context from the calling app
+     * @param packageName the name of the package to be checked
+     * @param userId target user ID.
+     * @return true if the package is default sms app or bluetooth
+     */
+    public static boolean isDefaultSmsApplicationAsUser(Context context, String packageName,
+            int userId) {
         if (packageName == null) {
             return false;
         }
-        final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context);
-        final String bluetoothPackageName = context.getResources()
+        ComponentName component = getDefaultSmsApplicationAsUser(context, false,
+                userId);
+        if (component == null) {
+            return false;
+        }
+
+        String defaultSmsPackage = component.getPackageName();
+        if (defaultSmsPackage == null) {
+            return false;
+        }
+
+        String bluetoothPackageName = context.getResources()
                 .getString(com.android.internal.R.string.config_systemBluetoothStack);
 
-        if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName))
-                || bluetoothPackageName.equals(packageName)) {
+        if (defaultSmsPackage.equals(packageName) || bluetoothPackageName.equals(packageName)) {
             return true;
         }
         return false;
     }
 
-    private static String getDefaultSmsApplicationPackageName(Context context) {
-        final ComponentName component = getDefaultSmsApplication(context, false);
-        if (component != null) {
-            return component.getPackageName();
-        }
-        return null;
-    }
-
     /**
      * Check if a package is default mms app (or equivalent, like bluetooth)
      *
@@ -1157,25 +1222,40 @@
      */
     @UnsupportedAppUsage
     public static boolean isDefaultMmsApplication(Context context, String packageName) {
+        return isDefaultMmsApplicationAsUser(context, packageName, getIncomingUserId());
+    }
+
+    /**
+     * Check if a package is default mms app (or equivalent, like bluetooth) on a given user.
+     *
+     * @param context context from the calling app
+     * @param packageName the name of the package to be checked
+     * @param userId target user ID.
+     * @return true if the package is default mms app or bluetooth
+     */
+    public static boolean isDefaultMmsApplicationAsUser(Context context, String packageName,
+            int userId) {
         if (packageName == null) {
             return false;
         }
-        String defaultMmsPackage = getDefaultMmsApplicationPackageName(context);
+
+        ComponentName component = getDefaultMmsApplicationAsUser(context, false,
+                userId);
+        if (component == null) {
+            return false;
+        }
+
+        String defaultMmsPackage = component.getPackageName();
+        if (defaultMmsPackage == null) {
+            return false;
+        }
+
         String bluetoothPackageName = context.getResources()
                 .getString(com.android.internal.R.string.config_systemBluetoothStack);
 
-        if ((defaultMmsPackage != null && defaultMmsPackage.equals(packageName))
-                || bluetoothPackageName.equals(packageName)) {
+        if (defaultMmsPackage.equals(packageName)|| bluetoothPackageName.equals(packageName)) {
             return true;
         }
         return false;
     }
-
-    private static String getDefaultMmsApplicationPackageName(Context context) {
-        ComponentName component = getDefaultMmsApplication(context, false);
-        if (component != null) {
-            return component.getPackageName();
-        }
-        return null;
-    }
-}
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
new file mode 100644
index 0000000..858cd76
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
@@ -0,0 +1,224 @@
+/*
+ * 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.wm.flicker.helpers;
+
+import android.annotation.NonNull;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+/**
+ * Injects gestures given an {@link Instrumentation} object.
+ */
+public class GestureHelper {
+    // Inserted after each motion event injection.
+    private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
+
+    private final UiAutomation mUiAutomation;
+
+    /**
+     * A pair of floating point values.
+     */
+    public static class Tuple {
+        public float x;
+        public float y;
+
+        public Tuple(float x, float y) {
+            this.x = x;
+            this.y = y;
+        }
+    }
+
+    public GestureHelper(Instrumentation instrumentation) {
+        mUiAutomation = instrumentation.getUiAutomation();
+    }
+
+    /**
+     * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture.
+     *
+     * @param startPoint1 initial coordinates of the first pointer
+     * @param startPoint2 initial coordinates of the second pointer
+     * @param endPoint1 final coordinates of the first pointer
+     * @param endPoint2 final coordinates of the second pointer
+     * @param steps number of steps to take to animate pinching
+     * @return true if gesture is injected successfully
+     */
+    public boolean pinch(@NonNull Tuple startPoint1, @NonNull Tuple startPoint2,
+            @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps) {
+        PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
+        PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER);
+
+        PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1);
+        PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1);
+
+        PointerProperties[] ptrProps = new PointerProperties[] {
+                ptrProp1, ptrProp2
+        };
+
+        PointerCoords[] ptrCoords = new PointerCoords[] {
+                ptrCoord1, ptrCoord2
+        };
+
+        long downTime = SystemClock.uptimeMillis();
+
+        if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) {
+            return false;
+        }
+
+        if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) {
+            return false;
+        }
+
+        if (!movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint1, endPoint2 },
+                downTime, steps)) {
+            return false;
+        }
+
+        if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) {
+            return false;
+        }
+
+        return primaryPointerUp(ptrProp1, ptrCoord1, downTime);
+    }
+
+    private boolean primaryPointerDown(@NonNull PointerProperties prop,
+            @NonNull PointerCoords coord, long downTime) {
+        MotionEvent event = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1,
+                new PointerProperties[]{ prop }, new PointerCoords[]{ coord });
+
+        return injectEventSync(event);
+    }
+
+    private boolean nonPrimaryPointerDown(@NonNull PointerProperties[] props,
+            @NonNull PointerCoords[] coords, long downTime, int index) {
+        // at least 2 pointers are needed
+        if (props.length != coords.length || coords.length < 2) {
+            return false;
+        }
+
+        long eventTime = SystemClock.uptimeMillis();
+
+        MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN
+                + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords);
+
+        return injectEventSync(event);
+    }
+
+    private boolean movePointers(@NonNull PointerProperties[] props,
+            @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps) {
+        // the number of endpoints should be the same as the number of pointers
+        if (props.length != coords.length || coords.length != endPoints.length) {
+            return false;
+        }
+
+        // prevent division by 0 and negative number of steps
+        if (steps < 1) {
+            steps = 1;
+        }
+
+        // save the starting points before updating any pointers
+        Tuple[] startPoints = new Tuple[coords.length];
+
+        for (int i = 0; i < coords.length; i++) {
+            startPoints[i] = new Tuple(coords[i].x, coords[i].y);
+        }
+
+        MotionEvent event;
+        long eventTime;
+
+        for (int i = 0; i < steps; i++) {
+            // inject a delay between movements
+            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+
+            // update the coordinates
+            for (int j = 0; j < coords.length; j++) {
+                coords[j].x += (endPoints[j].x - startPoints[j].x) / steps;
+                coords[j].y += (endPoints[j].y - startPoints[j].y) / steps;
+            }
+
+            eventTime = SystemClock.uptimeMillis();
+
+            event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE,
+                    coords.length, props, coords);
+
+            boolean didInject = injectEventSync(event);
+
+            if (!didInject) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private boolean primaryPointerUp(@NonNull PointerProperties prop,
+            @NonNull PointerCoords coord, long downTime) {
+        long eventTime = SystemClock.uptimeMillis();
+
+        MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_UP, 1,
+                new PointerProperties[]{ prop }, new PointerCoords[]{ coord });
+
+        return injectEventSync(event);
+    }
+
+    private boolean nonPrimaryPointerUp(@NonNull PointerProperties[] props,
+            @NonNull PointerCoords[] coords, long downTime, int index) {
+        // at least 2 pointers are needed
+        if (props.length != coords.length || coords.length < 2) {
+            return false;
+        }
+
+        long eventTime = SystemClock.uptimeMillis();
+
+        MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_UP
+                + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords);
+
+        return injectEventSync(event);
+    }
+
+    private PointerCoords getPointerCoord(float x, float y, float pressure, float size) {
+        PointerCoords ptrCoord = new PointerCoords();
+        ptrCoord.x = x;
+        ptrCoord.y = y;
+        ptrCoord.pressure = pressure;
+        ptrCoord.size = size;
+        return ptrCoord;
+    }
+
+    private PointerProperties getPointerProp(int id, int toolType) {
+        PointerProperties ptrProp = new PointerProperties();
+        ptrProp.id = id;
+        ptrProp.toolType = toolType;
+        return ptrProp;
+    }
+
+    private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
+            int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords) {
+        return MotionEvent.obtain(downTime, eventTime, action, pointerCount,
+                ptrProps, ptrCoords, 0, 0, 1.0f, 1.0f,
+                0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+    }
+
+    private boolean injectEventSync(InputEvent event) {
+        return mUiAutomation.injectInputEvent(event, true);
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index e730f31..19ee09a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -22,6 +22,7 @@
 import android.util.Log
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.GestureHelper.Tuple
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
@@ -44,6 +45,8 @@
         get() =
             mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` }
 
+    private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation)
+
     open fun clickObject(resId: String) {
         val selector = By.res(`package`, resId)
         val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object")
@@ -52,6 +55,50 @@
     }
 
     /**
+     * Expands the PIP window my using the pinch out gesture.
+     *
+     * @param percent The percentage by which to increase the pip window size.
+     * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f
+     */
+    fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) {
+        // the percentage must be between 0.0f and 1.0f
+        if (percent <= 0.0f || percent > 1.0f) {
+            throw IllegalArgumentException("Percent must be between 0.0f and 1.0f")
+        }
+
+        val windowRect = getWindowRect(wmHelper)
+
+        // first pointer's initial x coordinate is halfway between the left edge and the center
+        val initLeftX = (windowRect.centerX() - windowRect.width / 4).toFloat()
+        // second pointer's initial x coordinate is halfway between the right edge and the center
+        val initRightX = (windowRect.centerX() + windowRect.width / 4).toFloat()
+
+        // horizontal distance the window should increase by
+        val distIncrease = windowRect.width * percent
+
+        // final x-coordinates
+        val finalLeftX = initLeftX - (distIncrease / 2)
+        val finalRightX = initRightX + (distIncrease / 2)
+
+        // y-coordinate is the same throughout this animation
+        val yCoord = windowRect.centerY().toFloat()
+
+        var adjustedSteps = MIN_STEPS_TO_ANIMATE
+
+        // if distance per step is at least 1, then we can use the number of steps requested
+        if (distIncrease.toInt() / (steps * 2) >= 1) {
+            adjustedSteps = steps
+        }
+
+        // if the distance per step is less than 1, carry out the animation in two steps
+        gestureHelper.pinch(
+                Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord),
+                Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps)
+
+        waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
+    }
+
+    /**
      * Launches the app through an intent instead of interacting with the launcher and waits until
      * the app window is in PIP mode
      */
@@ -194,5 +241,8 @@
         private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start"
         private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual"
         private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter"
+        // minimum number of steps to take, when animating gestures, needs to be 2
+        // so that there is at least a single intermediate layer that flicker tests can check
+        private const val MIN_STEPS_TO_ANIMATE = 2
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
index 02b3b13..0ca6457 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
@@ -62,6 +62,7 @@
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
             tapl.setExpectedRotationCheckEnabled(false)
+            tapl.setIgnoreTaskbarVisibility(true)
             this.setRotation(testSpec.startRotation)
             testApp.launchViaIntent(wmHelper)
             wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
@@ -78,14 +79,16 @@
             imeTestApp.exit(wmHelper)
         }
         transitions {
-            // [Step1]: Swipe right from imeTestApp to testApp task
+            // [Step1]: Swipe right from testApp task to imeTestApp
             createTag(TAG_IME_VISIBLE)
+            // Expect taskBar invisible when switching to imeTestApp on the large screen device.
             tapl.launchedAppState.quickSwitchToPreviousApp()
             wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
             createTag(TAG_IME_INVISIBLE)
         }
         transitions {
-            // [Step2]: Swipe left to back to imeTestApp task
+            // [Step2]: Swipe left to back to testApp task
+            // Expect taskBar visible when switching to testApp on the large screen device.
             tapl.launchedAppState.quickSwitchToPreviousAppSwipeLeft()
             wmHelper.StateSyncBuilder().withFullScreenApp(imeTestApp).waitForAndVerify()
         }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
index 3f1a418..46186bc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
@@ -68,6 +68,7 @@
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
             tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setIgnoreTaskbarVisibility(true)
             testApp1.launchViaIntent(wmHelper)
             testApp2.launchViaIntent(wmHelper)
             startDisplayBounds =
diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
index 052ce3a..7a2af72 100644
--- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
+++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
@@ -153,6 +153,20 @@
     }
 
     @Test
+    public void testGetDefaultMmsApplication() {
+        assertEquals(TEST_COMPONENT_NAME,
+                SmsApplication.getDefaultMmsApplicationAsUser(mContext, false,
+                        UserHandle.USER_SYSTEM));
+    }
+
+    @Test
+    public void testGetDefaultExternalTelephonyProviderChangedApplication() {
+        assertEquals(TEST_COMPONENT_NAME,
+                SmsApplication.getDefaultExternalTelephonyProviderChangedApplicationAsUser(mContext,
+                        false, UserHandle.USER_SYSTEM));
+    }
+
+    @Test
     public void testGetDefaultSmsApplicationWithAppOpsFix() throws Exception {
         when(mAppOpsManager.unsafeCheckOp(AppOpsManager.OPSTR_READ_SMS, SMS_APP_UID,
                 TEST_COMPONENT_NAME.getPackageName()))
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 7ddbe95..aa337e5 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -105,6 +105,7 @@
         "format/Container.cpp",
         "format/binary/BinaryResourceParser.cpp",
         "format/binary/ResChunkPullParser.cpp",
+        "format/binary/ResEntryWriter.cpp",
         "format/binary/TableFlattener.cpp",
         "format/binary/XmlFlattener.cpp",
         "format/proto/ProtoDeserialize.cpp",
diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp
index 945f45b..41c7435 100644
--- a/tools/aapt2/ResourceUtils.cpp
+++ b/tools/aapt2/ResourceUtils.cpp
@@ -43,8 +43,9 @@
 static std::optional<ResourceNamedType> ToResourceNamedType(const char16_t* type16,
                                                             const char* type, size_t type_len) {
   std::optional<ResourceNamedTypeRef> parsed_type;
+  std::string converted;
   if (type16) {
-    auto converted = android::util::Utf16ToUtf8(StringPiece16(type16, type_len));
+    converted = android::util::Utf16ToUtf8(StringPiece16(type16, type_len));
     parsed_type = ParseResourceNamedType(converted);
   } else if (type) {
     parsed_type = ParseResourceNamedType(StringPiece(type, type_len));
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
new file mode 100644
index 0000000..8832c24
--- /dev/null
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -0,0 +1,278 @@
+/*
+ * 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.
+ */
+
+#include "format/binary/ResEntryWriter.h"
+
+#include "ValueVisitor.h"
+#include "androidfw/BigBuffer.h"
+#include "androidfw/ResourceTypes.h"
+#include "androidfw/Util.h"
+#include "format/binary/ResourceTypeExtensions.h"
+
+namespace aapt {
+
+using android::BigBuffer;
+using android::Res_value;
+using android::ResTable_entry;
+using android::ResTable_map;
+
+struct less_style_entries {
+  bool operator()(const Style::Entry* a, const Style::Entry* b) const {
+    if (a->key.id) {
+      if (b->key.id) {
+        return cmp_ids_dynamic_after_framework(a->key.id.value(), b->key.id.value());
+      }
+      return true;
+    }
+    if (!b->key.id) {
+      return a->key.name.value() < b->key.name.value();
+    }
+    return false;
+  }
+};
+
+class MapFlattenVisitor : public ConstValueVisitor {
+ public:
+  using ConstValueVisitor::Visit;
+
+  MapFlattenVisitor(ResTable_entry_ext* out_entry, BigBuffer* buffer)
+      : out_entry_(out_entry), buffer_(buffer) {
+  }
+
+  void Visit(const Attribute* attr) override {
+    {
+      Reference key = Reference(ResourceId(ResTable_map::ATTR_TYPE));
+      BinaryPrimitive val(Res_value::TYPE_INT_DEC, attr->type_mask);
+      FlattenEntry(&key, &val);
+    }
+
+    if (attr->min_int != std::numeric_limits<int32_t>::min()) {
+      Reference key = Reference(ResourceId(ResTable_map::ATTR_MIN));
+      BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->min_int));
+      FlattenEntry(&key, &val);
+    }
+
+    if (attr->max_int != std::numeric_limits<int32_t>::max()) {
+      Reference key = Reference(ResourceId(ResTable_map::ATTR_MAX));
+      BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->max_int));
+      FlattenEntry(&key, &val);
+    }
+
+    for (const Attribute::Symbol& s : attr->symbols) {
+      BinaryPrimitive val(s.type, s.value);
+      FlattenEntry(&s.symbol, &val);
+    }
+  }
+
+  void Visit(const Style* style) override {
+    if (style->parent) {
+      const Reference& parent_ref = style->parent.value();
+      CHECK(bool(parent_ref.id)) << "parent has no ID";
+      out_entry_->parent.ident = android::util::HostToDevice32(parent_ref.id.value().id);
+    }
+
+    // Sort the style.
+    std::vector<const Style::Entry*> sorted_entries;
+    for (const auto& entry : style->entries) {
+      sorted_entries.emplace_back(&entry);
+    }
+
+    std::sort(sorted_entries.begin(), sorted_entries.end(), less_style_entries());
+
+    for (const Style::Entry* entry : sorted_entries) {
+      FlattenEntry(&entry->key, entry->value.get());
+    }
+  }
+
+  void Visit(const Styleable* styleable) override {
+    for (auto& attr_ref : styleable->entries) {
+      BinaryPrimitive val(Res_value{});
+      FlattenEntry(&attr_ref, &val);
+    }
+  }
+
+  void Visit(const Array* array) override {
+    const size_t count = array->elements.size();
+    for (size_t i = 0; i < count; i++) {
+      Reference key(android::ResTable_map::ATTR_MIN + i);
+      FlattenEntry(&key, array->elements[i].get());
+    }
+  }
+
+  void Visit(const Plural* plural) override {
+    const size_t count = plural->values.size();
+    for (size_t i = 0; i < count; i++) {
+      if (!plural->values[i]) {
+        continue;
+      }
+
+      ResourceId q;
+      switch (i) {
+        case Plural::Zero:
+          q.id = android::ResTable_map::ATTR_ZERO;
+          break;
+
+        case Plural::One:
+          q.id = android::ResTable_map::ATTR_ONE;
+          break;
+
+        case Plural::Two:
+          q.id = android::ResTable_map::ATTR_TWO;
+          break;
+
+        case Plural::Few:
+          q.id = android::ResTable_map::ATTR_FEW;
+          break;
+
+        case Plural::Many:
+          q.id = android::ResTable_map::ATTR_MANY;
+          break;
+
+        case Plural::Other:
+          q.id = android::ResTable_map::ATTR_OTHER;
+          break;
+
+        default:
+          LOG(FATAL) << "unhandled plural type";
+          break;
+      }
+
+      Reference key(q);
+      FlattenEntry(&key, plural->values[i].get());
+    }
+  }
+
+  /**
+   * Call this after visiting a Value. This will finish any work that
+   * needs to be done to prepare the entry.
+   */
+  void Finish() {
+    out_entry_->count = android::util::HostToDevice32(entry_count_);
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(MapFlattenVisitor);
+
+  void FlattenKey(const Reference* key, ResTable_map* out_entry) {
+    CHECK(bool(key->id)) << "key has no ID";
+    out_entry->name.ident = android::util::HostToDevice32(key->id.value().id);
+  }
+
+  void FlattenValue(const Item* value, ResTable_map* out_entry) {
+    CHECK(value->Flatten(&out_entry->value)) << "flatten failed";
+  }
+
+  void FlattenEntry(const Reference* key, Item* value) {
+    ResTable_map* out_entry = buffer_->NextBlock<ResTable_map>();
+    FlattenKey(key, out_entry);
+    FlattenValue(value, out_entry);
+    out_entry->value.size = android::util::HostToDevice16(sizeof(out_entry->value));
+    entry_count_++;
+  }
+
+  ResTable_entry_ext* out_entry_;
+  BigBuffer* buffer_;
+  size_t entry_count_ = 0;
+};
+
+template <typename T>
+void WriteEntry(const FlatEntry* entry, T* out_result) {
+  static_assert(std::is_same_v<ResTable_entry, T> || std::is_same_v<ResTable_entry_ext, T>,
+                "T must be ResTable_entry or ResTable_entry_ext");
+
+  ResTable_entry* out_entry = (ResTable_entry*)out_result;
+  if (entry->entry->visibility.level == Visibility::Level::kPublic) {
+    out_entry->flags |= ResTable_entry::FLAG_PUBLIC;
+  }
+
+  if (entry->value->IsWeak()) {
+    out_entry->flags |= ResTable_entry::FLAG_WEAK;
+  }
+
+  if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
+    out_entry->flags |= ResTable_entry::FLAG_COMPLEX;
+  }
+
+  out_entry->flags = android::util::HostToDevice16(out_entry->flags);
+  out_entry->key.index = android::util::HostToDevice32(entry->entry_key);
+  out_entry->size = android::util::HostToDevice16(sizeof(T));
+}
+
+int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer) {
+  int32_t offset = buffer->size();
+  ResTable_entry_ext* out_entry = buffer->NextBlock<ResTable_entry_ext>();
+  WriteEntry<ResTable_entry_ext>(map_entry, out_entry);
+
+  MapFlattenVisitor visitor(out_entry, buffer);
+  map_entry->value->Accept(&visitor);
+  visitor.Finish();
+  return offset;
+}
+
+void WriteItemToPair(const FlatEntry* item_entry, ResEntryValuePair* out_pair) {
+  static_assert(sizeof(ResEntryValuePair) == sizeof(ResTable_entry) + sizeof(Res_value),
+                "ResEntryValuePair must not have padding between entry and value.");
+
+  WriteEntry<ResTable_entry>(item_entry, &out_pair->entry);
+
+  CHECK(ValueCast<Item>(item_entry->value)->Flatten(&out_pair->value)) << "flatten failed";
+  out_pair->value.size = android::util::HostToDevice16(sizeof(out_pair->value));
+}
+
+int32_t SequentialResEntryWriter::WriteMap(const FlatEntry* entry) {
+  return WriteMapToBuffer(entry, entries_buffer_);
+}
+
+int32_t SequentialResEntryWriter::WriteItem(const FlatEntry* entry) {
+  int32_t offset = entries_buffer_->size();
+  auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>();
+  WriteItemToPair(entry, out_pair);
+  return offset;
+}
+
+std::size_t ResEntryValuePairContentHasher::operator()(const ResEntryValuePairRef& ref) const {
+  return android::JenkinsHashMixBytes(0, ref.ptr, sizeof(ResEntryValuePair));
+}
+
+bool ResEntryValuePairContentEqualTo::operator()(const ResEntryValuePairRef& a,
+                                                 const ResEntryValuePairRef& b) const {
+  return std::memcmp(a.ptr, b.ptr, sizeof(ResEntryValuePair)) == 0;
+}
+
+int32_t DeduplicateItemsResEntryWriter::WriteMap(const FlatEntry* entry) {
+  return WriteMapToBuffer(entry, entries_buffer_);
+}
+
+int32_t DeduplicateItemsResEntryWriter::WriteItem(const FlatEntry* entry) {
+  int32_t initial_offset = entries_buffer_->size();
+
+  auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>();
+  WriteItemToPair(entry, out_pair);
+
+  auto ref = ResEntryValuePairRef{*out_pair};
+  auto [it, inserted] = entry_offsets.insert({ref, initial_offset});
+  if (inserted) {
+    // If inserted just return a new offset as this is a first time we store
+    // this entry.
+    return initial_offset;
+  }
+  // If not inserted this means that this is a duplicate, backup allocated block to the buffer
+  // and return offset of previously stored entry.
+  entries_buffer_->BackUp(sizeof(ResEntryValuePair));
+  return it->second;
+}
+
+}  // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
new file mode 100644
index 0000000..a36ceec
--- /dev/null
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+
+#ifndef AAPT_FORMAT_BINARY_RESENTRY_SERIALIZER_H
+#define AAPT_FORMAT_BINARY_RESENTRY_SERIALIZER_H
+
+#include <unordered_map>
+
+#include "ResourceTable.h"
+#include "ValueVisitor.h"
+#include "android-base/macros.h"
+#include "androidfw/BigBuffer.h"
+#include "androidfw/ResourceTypes.h"
+
+namespace aapt {
+
+struct FlatEntry {
+  const ResourceTableEntryView* entry;
+  const Value* value;
+
+  // The entry string pool index to the entry's name.
+  uint32_t entry_key;
+};
+
+// Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer.
+// We introduce this structure for ResEntryWriter to a have single allocation using
+// BigBuffer::NextBlock which allows to return it back with BigBuffer::Backup.
+struct ResEntryValuePair {
+  android::ResTable_entry entry;
+  android::Res_value value;
+};
+
+// References ResEntryValuePair object stored in BigBuffer used as a key in std::unordered_map.
+// Allows access to memory address where ResEntryValuePair is stored.
+union ResEntryValuePairRef {
+  const std::reference_wrapper<const ResEntryValuePair> pair;
+  const u_char* ptr;
+
+  explicit ResEntryValuePairRef(const ResEntryValuePair& ref) : pair(ref) {
+  }
+};
+
+// Hasher which computes hash of ResEntryValuePair using its bytes representation in memory.
+struct ResEntryValuePairContentHasher {
+  std::size_t operator()(const ResEntryValuePairRef& ref) const;
+};
+
+// Equaler which compares ResEntryValuePairs using theirs bytes representation in memory.
+struct ResEntryValuePairContentEqualTo {
+  bool operator()(const ResEntryValuePairRef& a, const ResEntryValuePairRef& b) const;
+};
+
+// Base class that allows to write FlatEntries into entries_buffer.
+class ResEntryWriter {
+ public:
+  virtual ~ResEntryWriter() = default;
+
+  // Writes resource table entry and its value into 'entries_buffer_' and returns offset
+  // in the buffer where entry was written.
+  int32_t Write(const FlatEntry* entry) {
+    if (ValueCast<Item>(entry->value) != nullptr) {
+      return WriteItem(entry);
+    } else {
+      return WriteMap(entry);
+    }
+  }
+
+ protected:
+  ResEntryWriter(android::BigBuffer* entries_buffer) : entries_buffer_(entries_buffer) {
+  }
+  android::BigBuffer* entries_buffer_;
+
+  virtual int32_t WriteItem(const FlatEntry* entry) = 0;
+
+  virtual int32_t WriteMap(const FlatEntry* entry) = 0;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ResEntryWriter);
+};
+
+// ResEntryWriter which writes FlatEntries sequentially into entries_buffer.
+// Next entry is always written right after previous one in the buffer.
+class SequentialResEntryWriter : public ResEntryWriter {
+ public:
+  explicit SequentialResEntryWriter(android::BigBuffer* entries_buffer)
+      : ResEntryWriter(entries_buffer) {
+  }
+  ~SequentialResEntryWriter() override = default;
+
+  int32_t WriteItem(const FlatEntry* entry) override;
+
+  int32_t WriteMap(const FlatEntry* entry) override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(SequentialResEntryWriter);
+};
+
+// ResEntryWriter that writes only unique entry and value pairs into entries_buffer.
+// Next entry is written into buffer only if there is no entry with the same bytes representation
+// in memory written before. Otherwise returns offset of already written entry.
+class DeduplicateItemsResEntryWriter : public ResEntryWriter {
+ public:
+  explicit DeduplicateItemsResEntryWriter(android::BigBuffer* entries_buffer)
+      : ResEntryWriter(entries_buffer) {
+  }
+  ~DeduplicateItemsResEntryWriter() override = default;
+
+  int32_t WriteItem(const FlatEntry* entry) override;
+
+  int32_t WriteMap(const FlatEntry* entry) override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(DeduplicateItemsResEntryWriter);
+
+  std::unordered_map<ResEntryValuePairRef, int32_t, ResEntryValuePairContentHasher,
+                     ResEntryValuePairContentEqualTo>
+      entry_offsets;
+};
+
+}  // namespace aapt
+
+#endif
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/ResEntryWriter_test.cpp b/tools/aapt2/format/binary/ResEntryWriter_test.cpp
new file mode 100644
index 0000000..56ca133
--- /dev/null
+++ b/tools/aapt2/format/binary/ResEntryWriter_test.cpp
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+#include "format/binary/ResEntryWriter.h"
+
+#include "androidfw/BigBuffer.h"
+#include "format/binary/ResourceTypeExtensions.h"
+#include "test/Test.h"
+#include "util/Util.h"
+
+using ::android::BigBuffer;
+using ::android::Res_value;
+using ::android::ResTable_map;
+using ::testing::Eq;
+using ::testing::Ge;
+using ::testing::IsNull;
+using ::testing::Ne;
+using ::testing::NotNull;
+
+namespace aapt {
+
+using SequentialResEntryWriterTest = CommandTestFixture;
+using DeduplicateItemsResEntryWriterTest = CommandTestFixture;
+
+std::vector<int32_t> WriteAllEntries(const ResourceTableView& table, ResEntryWriter& writer) {
+  std::vector<int32_t> result = {};
+  for (const auto& type : table.packages[0].types) {
+    for (const auto& entry : type.entries) {
+      for (const auto& value : entry.values) {
+        auto flat_entry = FlatEntry{&entry, value->value.get(), 0};
+        result.push_back(writer.Write(&flat_entry));
+      }
+    }
+  }
+  return result;
+}
+
+TEST_F(SequentialResEntryWriterTest, WriteEntriesOneByOne) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .AddSimple("com.app.test:id/id1", ResourceId(0x7f010000))
+          .AddSimple("com.app.test:id/id2", ResourceId(0x7f010001))
+          .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
+          .Build();
+
+  BigBuffer out(512);
+  SequentialResEntryWriter writer(&out);
+  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+  std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair),
+                                        2 * sizeof(ResEntryValuePair)};
+  EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair));
+  EXPECT_EQ(offsets, expected_offsets);
+};
+
+TEST_F(SequentialResEntryWriterTest, WriteMapEntriesOneByOne) {
+  std::unique_ptr<Array> array1 = util::make_unique<Array>();
+  array1->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+  array1->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+  std::unique_ptr<Array> array2 = util::make_unique<Array>();
+  array2->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+  array2->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+                                             .AddValue("com.app.test:array/arr1", std::move(array1))
+                                             .AddValue("com.app.test:array/arr2", std::move(array2))
+                                             .Build();
+
+  BigBuffer out(512);
+  SequentialResEntryWriter writer(&out);
+  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+  std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+  EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+  EXPECT_EQ(offsets, expected_offsets);
+};
+
+TEST_F(DeduplicateItemsResEntryWriterTest, DeduplicateItemEntries) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .AddSimple("com.app.test:id/id1", ResourceId(0x7f010000))
+          .AddSimple("com.app.test:id/id2", ResourceId(0x7f010001))
+          .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
+          .Build();
+
+  BigBuffer out(512);
+  DeduplicateItemsResEntryWriter writer(&out);
+  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+  std::vector<int32_t> expected_offsets{0, 0, 0};
+  EXPECT_EQ(out.size(), sizeof(ResEntryValuePair));
+  EXPECT_EQ(offsets, expected_offsets);
+};
+
+TEST_F(DeduplicateItemsResEntryWriterTest, WriteMapEntriesOneByOne) {
+  std::unique_ptr<Array> array1 = util::make_unique<Array>();
+  array1->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+  array1->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+  std::unique_ptr<Array> array2 = util::make_unique<Array>();
+  array2->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+  array2->elements.push_back(
+      util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+                                             .AddValue("com.app.test:array/arr1", std::move(array1))
+                                             .AddValue("com.app.test:array/arr2", std::move(array2))
+                                             .Build();
+
+  BigBuffer out(512);
+  DeduplicateItemsResEntryWriter writer(&out);
+  auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+  std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+  EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+  EXPECT_EQ(offsets, expected_offsets);
+};
+
+}  // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 22f278c..7dc9d26 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -16,21 +16,20 @@
 
 #include "format/binary/TableFlattener.h"
 
-#include <algorithm>
-#include <numeric>
 #include <sstream>
 #include <type_traits>
+#include <variant>
 
 #include "ResourceTable.h"
 #include "ResourceValues.h"
 #include "SdkConstants.h"
-#include "ValueVisitor.h"
 #include "android-base/logging.h"
 #include "android-base/macros.h"
 #include "android-base/stringprintf.h"
 #include "androidfw/BigBuffer.h"
 #include "androidfw/ResourceUtils.h"
 #include "format/binary/ChunkWriter.h"
+#include "format/binary/ResEntryWriter.h"
 #include "format/binary/ResourceTypeExtensions.h"
 #include "trace/TraceBuffer.h"
 
@@ -58,170 +57,6 @@
   dst[i] = 0;
 }
 
-static bool cmp_style_entries(const Style::Entry* a, const Style::Entry* b) {
-  if (a->key.id) {
-    if (b->key.id) {
-      return cmp_ids_dynamic_after_framework(a->key.id.value(), b->key.id.value());
-    }
-    return true;
-  } else if (!b->key.id) {
-    return a->key.name.value() < b->key.name.value();
-  }
-  return false;
-}
-
-struct FlatEntry {
-  const ResourceTableEntryView* entry;
-  const Value* value;
-
-  // The entry string pool index to the entry's name.
-  uint32_t entry_key;
-};
-
-class MapFlattenVisitor : public ConstValueVisitor {
- public:
-  using ConstValueVisitor::Visit;
-
-  MapFlattenVisitor(ResTable_entry_ext* out_entry, BigBuffer* buffer)
-      : out_entry_(out_entry), buffer_(buffer) {
-  }
-
-  void Visit(const Attribute* attr) override {
-    {
-      Reference key = Reference(ResourceId(ResTable_map::ATTR_TYPE));
-      BinaryPrimitive val(Res_value::TYPE_INT_DEC, attr->type_mask);
-      FlattenEntry(&key, &val);
-    }
-
-    if (attr->min_int != std::numeric_limits<int32_t>::min()) {
-      Reference key = Reference(ResourceId(ResTable_map::ATTR_MIN));
-      BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->min_int));
-      FlattenEntry(&key, &val);
-    }
-
-    if (attr->max_int != std::numeric_limits<int32_t>::max()) {
-      Reference key = Reference(ResourceId(ResTable_map::ATTR_MAX));
-      BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->max_int));
-      FlattenEntry(&key, &val);
-    }
-
-    for (const Attribute::Symbol& s : attr->symbols) {
-      BinaryPrimitive val(s.type, s.value);
-      FlattenEntry(&s.symbol, &val);
-    }
-  }
-
-  void Visit(const Style* style) override {
-    if (style->parent) {
-      const Reference& parent_ref = style->parent.value();
-      CHECK(bool(parent_ref.id)) << "parent has no ID";
-      out_entry_->parent.ident = android::util::HostToDevice32(parent_ref.id.value().id);
-    }
-
-    // Sort the style.
-    std::vector<const Style::Entry*> sorted_entries;
-    for (const auto& entry : style->entries) {
-      sorted_entries.emplace_back(&entry);
-    }
-
-    std::sort(sorted_entries.begin(), sorted_entries.end(), cmp_style_entries);
-
-    for (const Style::Entry* entry : sorted_entries) {
-      FlattenEntry(&entry->key, entry->value.get());
-    }
-  }
-
-  void Visit(const Styleable* styleable) override {
-    for (auto& attr_ref : styleable->entries) {
-      BinaryPrimitive val(Res_value{});
-      FlattenEntry(&attr_ref, &val);
-    }
-  }
-
-  void Visit(const Array* array) override {
-    const size_t count = array->elements.size();
-    for (size_t i = 0; i < count; i++) {
-      Reference key(android::ResTable_map::ATTR_MIN + i);
-      FlattenEntry(&key, array->elements[i].get());
-    }
-  }
-
-  void Visit(const Plural* plural) override {
-    const size_t count = plural->values.size();
-    for (size_t i = 0; i < count; i++) {
-      if (!plural->values[i]) {
-        continue;
-      }
-
-      ResourceId q;
-      switch (i) {
-        case Plural::Zero:
-          q.id = android::ResTable_map::ATTR_ZERO;
-          break;
-
-        case Plural::One:
-          q.id = android::ResTable_map::ATTR_ONE;
-          break;
-
-        case Plural::Two:
-          q.id = android::ResTable_map::ATTR_TWO;
-          break;
-
-        case Plural::Few:
-          q.id = android::ResTable_map::ATTR_FEW;
-          break;
-
-        case Plural::Many:
-          q.id = android::ResTable_map::ATTR_MANY;
-          break;
-
-        case Plural::Other:
-          q.id = android::ResTable_map::ATTR_OTHER;
-          break;
-
-        default:
-          LOG(FATAL) << "unhandled plural type";
-          break;
-      }
-
-      Reference key(q);
-      FlattenEntry(&key, plural->values[i].get());
-    }
-  }
-
-  /**
-   * Call this after visiting a Value. This will finish any work that
-   * needs to be done to prepare the entry.
-   */
-  void Finish() {
-    out_entry_->count = android::util::HostToDevice32(entry_count_);
-  }
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(MapFlattenVisitor);
-
-  void FlattenKey(const Reference* key, ResTable_map* out_entry) {
-    CHECK(bool(key->id)) << "key has no ID";
-    out_entry->name.ident = android::util::HostToDevice32(key->id.value().id);
-  }
-
-  void FlattenValue(const Item* value, ResTable_map* out_entry) {
-    CHECK(value->Flatten(&out_entry->value)) << "flatten failed";
-  }
-
-  void FlattenEntry(const Reference* key, Item* value) {
-    ResTable_map* out_entry = buffer_->NextBlock<ResTable_map>();
-    FlattenKey(key, out_entry);
-    FlattenValue(value, out_entry);
-    out_entry->value.size = android::util::HostToDevice16(sizeof(out_entry->value));
-    entry_count_++;
-  }
-
-  ResTable_entry_ext* out_entry_;
-  BigBuffer* buffer_;
-  size_t entry_count_ = 0;
-};
-
 struct OverlayableChunk {
   std::string actor;
   android::Source source;
@@ -233,14 +68,16 @@
   PackageFlattener(IAaptContext* context, const ResourceTablePackageView& package,
                    const std::map<size_t, std::string>* shared_libs,
                    SparseEntriesMode sparse_entries, bool collapse_key_stringpool,
-                   const std::set<ResourceName>& name_collapse_exemptions)
+                   const std::set<ResourceName>& name_collapse_exemptions,
+                   bool deduplicate_entry_values)
       : context_(context),
         diag_(context->GetDiagnostics()),
         package_(package),
         shared_libs_(shared_libs),
         sparse_entries_(sparse_entries),
         collapse_key_stringpool_(collapse_key_stringpool),
-        name_collapse_exemptions_(name_collapse_exemptions) {
+        name_collapse_exemptions_(name_collapse_exemptions),
+        deduplicate_entry_values_(deduplicate_entry_values) {
   }
 
   bool FlattenPackage(BigBuffer* buffer) {
@@ -298,47 +135,6 @@
  private:
   DISALLOW_COPY_AND_ASSIGN(PackageFlattener);
 
-  template <typename T, bool IsItem>
-  T* WriteEntry(FlatEntry* entry, BigBuffer* buffer) {
-    static_assert(
-        std::is_same<ResTable_entry, T>::value || std::is_same<ResTable_entry_ext, T>::value,
-        "T must be ResTable_entry or ResTable_entry_ext");
-
-    T* result = buffer->NextBlock<T>();
-    ResTable_entry* out_entry = (ResTable_entry*)result;
-    if (entry->entry->visibility.level == Visibility::Level::kPublic) {
-      out_entry->flags |= ResTable_entry::FLAG_PUBLIC;
-    }
-
-    if (entry->value->IsWeak()) {
-      out_entry->flags |= ResTable_entry::FLAG_WEAK;
-    }
-
-    if (!IsItem) {
-      out_entry->flags |= ResTable_entry::FLAG_COMPLEX;
-    }
-
-    out_entry->flags = android::util::HostToDevice16(out_entry->flags);
-    out_entry->key.index = android::util::HostToDevice32(entry->entry_key);
-    out_entry->size = android::util::HostToDevice16(sizeof(T));
-    return result;
-  }
-
-  bool FlattenValue(FlatEntry* entry, BigBuffer* buffer) {
-    if (const Item* item = ValueCast<Item>(entry->value)) {
-      WriteEntry<ResTable_entry, true>(entry, buffer);
-      Res_value* outValue = buffer->NextBlock<Res_value>();
-      CHECK(item->Flatten(outValue)) << "flatten failed";
-      outValue->size = android::util::HostToDevice16(sizeof(*outValue));
-    } else {
-      ResTable_entry_ext* out_entry = WriteEntry<ResTable_entry_ext, false>(entry, buffer);
-      MapFlattenVisitor visitor(out_entry, buffer);
-      entry->value->Accept(&visitor);
-      visitor.Finish();
-    }
-    return true;
-  }
-
   bool FlattenConfig(const ResourceTableTypeView& type, const ConfigDescription& config,
                      const size_t num_total_entries, std::vector<FlatEntry>* entries,
                      BigBuffer* buffer) {
@@ -355,16 +151,18 @@
     offsets.resize(num_total_entries, 0xffffffffu);
 
     android::BigBuffer values_buffer(512);
+    std::variant<std::monostate, DeduplicateItemsResEntryWriter, SequentialResEntryWriter>
+        writer_variant;
+    ResEntryWriter* res_entry_writer;
+    if (deduplicate_entry_values_) {
+      res_entry_writer = &writer_variant.emplace<DeduplicateItemsResEntryWriter>(&values_buffer);
+    } else {
+      res_entry_writer = &writer_variant.emplace<SequentialResEntryWriter>(&values_buffer);
+    }
+
     for (FlatEntry& flat_entry : *entries) {
       CHECK(static_cast<size_t>(flat_entry.entry->id.value()) < num_total_entries);
-      offsets[flat_entry.entry->id.value()] = values_buffer.size();
-      if (!FlattenValue(&flat_entry, &values_buffer)) {
-        diag_->Error(android::DiagMessage()
-                     << "failed to flatten resource '"
-                     << ResourceNameRef(package_.name, type.named_type, flat_entry.entry->name)
-                     << "' for configuration '" << config << "'");
-        return false;
-      }
+      offsets[flat_entry.entry->id.value()] = res_entry_writer->Write(&flat_entry);
     }
 
     bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled ||
@@ -720,6 +518,7 @@
   bool collapse_key_stringpool_;
   const std::set<ResourceName>& name_collapse_exemptions_;
   std::map<uint32_t, uint32_t> aliases_;
+  bool deduplicate_entry_values_;
 };
 
 }  // namespace
@@ -771,7 +570,8 @@
 
     PackageFlattener flattener(context, package, &table->included_packages_,
                                options_.sparse_entries, options_.collapse_key_stringpool,
-                               options_.name_collapse_exemptions);
+                               options_.name_collapse_exemptions,
+                               options_.deduplicate_entry_values);
     if (!flattener.FlattenPackage(&package_buffer)) {
       return false;
     }
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index c6d3033..6151b7e 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -54,6 +54,20 @@
 
   // Map from original resource paths to shortened resource paths.
   std::map<std::string, std::string> shortened_path_map;
+
+  // When enabled, only unique pairs of entry and value are stored in type chunks.
+  //
+  // By default, all such pairs are unique because a reference to resource name in the string pool
+  // is a part of the pair. But when resource names are collapsed (using 'collapse_key_stringpool'
+  // flag or manually) the same data might be duplicated multiple times in the same type chunk.
+  //
+  // For example: an application has 3 boolean resources with collapsed names and 3 'true' values
+  // are defined for these resources in 'default' configuration. All pairs of entry and value for
+  // these resources will have the same binary representation and stored only once in type chunk
+  // instead of three times when this flag is disabled.
+  //
+  // This applies only to simple entries (entry->flags & ResTable_entry::FLAG_COMPLEX == 0).
+  bool deduplicate_entry_values = false;
 };
 
 class TableFlattener : public IResourceTableConsumer {
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index b69dadd..2097a63 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -669,6 +669,87 @@
                      ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)*idx, 0u));
 }
 
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithDeduplicationSucceeds) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .AddSimple("com.app.test:id/one", ResourceId(0x7f020000))
+          .AddSimple("com.app.test:id/two", ResourceId(0x7f020001))
+          .AddValue("com.app.test:id/three", ResourceId(0x7f020002),
+                    test::BuildReference("com.app.test:id/one", ResourceId(0x7f020000)))
+          .AddValue("com.app.test:integer/one", ResourceId(0x7f030000),
+                    util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u))
+          .AddValue("com.app.test:integer/one", test::ParseConfigOrDie("v1"),
+                    ResourceId(0x7f030000),
+                    util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u))
+          .AddString("com.app.test:string/test1", ResourceId(0x7f040000), "foo")
+          .AddString("com.app.test:string/test2", ResourceId(0x7f040001), "foo")
+          .AddString("com.app.test:string/test3", ResourceId(0x7f040002), "bar")
+          .AddString("com.app.test:string/test4", ResourceId(0x7f040003), "foo")
+          .AddString("com.app.test:layout/bar1", ResourceId(0x7f050000), "res/layout/bar.xml")
+          .AddString("com.app.test:layout/bar2", ResourceId(0x7f050001), "res/layout/bar.xml")
+          .Build();
+
+  TableFlattenerOptions options;
+  options.collapse_key_stringpool = true;
+  options.deduplicate_entry_values = true;
+
+  ResTable res_table;
+
+  ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+                     ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
+                     ResTable_config::CONFIG_VERSION));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+                     ResourceId(0x7f030000), test::ParseConfigOrDie("v1"), Res_value::TYPE_INT_DEC,
+                     2u, ResTable_config::CONFIG_VERSION));
+
+  std::u16string foo_str = u"foo";
+  std::u16string bar_str = u"bar";
+  auto foo_idx = res_table.getTableStringBlock(0)->indexOfString(foo_str.data(), foo_str.size());
+  auto bar_idx = res_table.getTableStringBlock(0)->indexOfString(bar_str.data(), bar_str.size());
+  ASSERT_TRUE(foo_idx.has_value());
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+                     ResourceId(0x7f040000), {}, Res_value::TYPE_STRING, (uint32_t)*foo_idx, 0u));
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+                     ResourceId(0x7f040001), {}, Res_value::TYPE_STRING, (uint32_t)*foo_idx, 0u));
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+                     ResourceId(0x7f040002), {}, Res_value::TYPE_STRING, (uint32_t)*bar_idx, 0u));
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+                     ResourceId(0x7f040003), {}, Res_value::TYPE_STRING, (uint32_t)*foo_idx, 0u));
+
+  std::u16string bar_path = u"res/layout/bar.xml";
+  auto bar_path_idx =
+      res_table.getTableStringBlock(0)->indexOfString(bar_path.data(), bar_path.size());
+  ASSERT_TRUE(bar_path_idx.has_value());
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+                     ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)*bar_path_idx,
+                     0u));
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+                     ResourceId(0x7f050001), {}, Res_value::TYPE_STRING, (uint32_t)*bar_path_idx,
+                     0u));
+
+  std::string deduplicated_output;
+  std::string sequential_output;
+  Flatten(context_.get(), options, table.get(), &deduplicated_output);
+  options.deduplicate_entry_values = false;
+  Flatten(context_.get(), options, table.get(), &sequential_output);
+
+  // We have 4 duplicates: 0x7f020001 id, 0x7f040001 string, 0x7f040003 string, 0x7f050001 layout.
+  EXPECT_EQ(sequential_output.size(),
+            deduplicated_output.size() + 4 * (sizeof(ResTable_entry) + sizeof(Res_value)));
+}
+
 TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithNameCollapseExemptionsSucceeds) {
   std::unique_ptr<ResourceTable> table =
       test::ResourceTableBuilder()
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
index 8aa3e25..4d69d26 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -42,6 +42,8 @@
         SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
         PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
         RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG,
+        PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
+        PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
     )
 
     override val api: Int
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
index 82eb8ed..3d5d01c 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
@@ -34,3 +34,7 @@
         Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"),
         Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission")
 )
+
+const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod"
+const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName"
+const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult"
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
new file mode 100644
index 0000000..68a450d
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getContainingUMethod
+
+/**
+ * Stops incorrect usage of {@link PermissionMethod}
+ * TODO: add tests once re-enabled (b/240445172, b/247542171)
+ */
+class PermissionMethodDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableUastTypes(): List<Class<out UElement>> =
+        listOf(UAnnotation::class.java, UMethod::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler =
+        PermissionMethodHandler(context)
+
+    private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
+        override fun visitMethod(node: UMethod) {
+            if (hasPermissionMethodAnnotation(node)) return
+            if (onlyCallsPermissionMethod(node)) {
+                val location = context.getLocation(node.javaPsi.modifierList)
+                val fix = fix()
+                    .annotate(ANNOTATION_PERMISSION_METHOD)
+                    .range(location)
+                    .autoFix()
+                    .build()
+
+                context.report(
+                    ISSUE_CAN_BE_PERMISSION_METHOD,
+                    location,
+                    "Annotate method with @PermissionMethod",
+                    fix
+                )
+            }
+        }
+
+        override fun visitAnnotation(node: UAnnotation) {
+            if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
+            val method = node.getContainingUMethod() ?: return
+
+            if (!isPermissionMethodReturnType(method)) {
+                context.report(
+                    ISSUE_PERMISSION_METHOD_USAGE,
+                    context.getLocation(node),
+                    """
+                            Methods annotated with `@PermissionMethod` should return `void`, \
+                            `boolean`, or `@PackageManager.PermissionResult int`."
+                    """.trimIndent()
+                )
+            }
+
+            if (method.returnType == PsiType.INT &&
+                method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
+            ) {
+                context.report(
+                    ISSUE_PERMISSION_METHOD_USAGE,
+                    context.getLocation(node),
+                    """
+                            Methods annotated with `@PermissionMethod` that return `int` should \
+                            also be annotated with `@PackageManager.PermissionResult.`"
+                    """.trimIndent()
+                )
+            }
+        }
+    }
+
+    companion object {
+
+        private val EXPLANATION_PERMISSION_METHOD_USAGE = """
+            `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
+            Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
+            `void` and potentially throw `SecurityException`.
+        """.trimIndent()
+
+        @JvmField
+        val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
+            id = "PermissionMethodUsage",
+            briefDescription = "@PermissionMethod used incorrectly",
+            explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
+            category = Category.CORRECTNESS,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                PermissionMethodDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+            enabledByDefault = true
+        )
+
+        private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
+            Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
+            be annotated with @PermissionMethod.  For example:
+            ```
+            void wrapperHelper() {
+              // Context.enforceCallingPermission is annotated with @PermissionMethod
+              context.enforceCallingPermission(SOME_PERMISSION)
+            }
+            ```
+        """.trimIndent()
+
+        @JvmField
+        val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
+            id = "CanBePermissionMethod",
+            briefDescription = "Method can be annotated with @PermissionMethod",
+            explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
+            category = Category.SECURITY,
+            priority = 5,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                PermissionMethodDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+            enabledByDefault = false
+        )
+
+        private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+            .any {
+                it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
+            }
+
+        private fun isPermissionMethodReturnType(method: UMethod): Boolean =
+            listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
+
+        /**
+         * Identifies methods that...
+         * DO call other methods annotated with @PermissionMethod
+         * DO NOT do anything else
+         */
+        private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
+            val body = method.uastBody as? UBlockExpression ?: return false
+            if (body.expressions.isEmpty()) return false
+            for (expression in body.expressions) {
+                when (expression) {
+                    is UQualifiedReferenceExpression -> {
+                        if (!isPermissionMethodCall(expression.selector)) return false
+                    }
+                    is UReturnExpression -> {
+                        if (!isPermissionMethodCall(expression.returnExpression)) return false
+                    }
+                    is UCallExpression -> {
+                        if (!isPermissionMethodCall(expression)) return false
+                    }
+                    is UIfExpression -> {
+                        if (expression.thenExpression !is UReturnExpression) return false
+                        if (!isPermissionMethodCall(expression.condition)) return false
+                    }
+                    else -> return false
+                }
+            }
+            return true
+        }
+
+        private fun isPermissionMethodCall(expression: UExpression?): Boolean {
+            return when (expression) {
+                is UQualifiedReferenceExpression ->
+                    return isPermissionMethodCall(expression.selector)
+                is UCallExpression -> {
+                    val calledMethod = expression.resolve()?.getUMethod() ?: return false
+                    return hasPermissionMethodAnnotation(calledMethod)
+                }
+                else -> false
+            }
+        }
+    }
+}
diff --git a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
index 3ab09a8..dfebdcc 100644
--- a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
+++ b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
@@ -37,10 +37,11 @@
 class ImmutabilityProcessor : AbstractProcessor() {
 
     companion object {
+
         /**
-         * Types that are already immutable.
+         * Types that are already immutable. Will also ignore subclasses.
          */
-        private val IGNORED_TYPES = listOf(
+        private val IGNORED_SUPER_TYPES = listOf(
             "java.io.File",
             "java.lang.Boolean",
             "java.lang.Byte",
@@ -56,6 +57,15 @@
             "android.os.Parcelable.Creator",
         )
 
+        /**
+         * Types that are already immutable. Must be an exact match, does not include any super
+         * or sub classes.
+         */
+        private val IGNORED_EXACT_TYPES = listOf(
+            "java.lang.Class",
+            "java.lang.Object",
+        )
+
         private val IGNORED_METHODS = listOf(
             "writeToParcel",
         )
@@ -64,7 +74,8 @@
     private lateinit var collectionType: TypeMirror
     private lateinit var mapType: TypeMirror
 
-    private lateinit var ignoredTypes: List<TypeMirror>
+    private lateinit var ignoredSuperTypes: List<TypeMirror>
+    private lateinit var ignoredExactTypes: List<TypeMirror>
 
     private val seenTypesByPolicy = mutableMapOf<Set<Immutable.Policy.Exception>, Set<Type>>()
 
@@ -76,7 +87,8 @@
         super.init(processingEnv)
         collectionType = processingEnv.erasedType("java.util.Collection")!!
         mapType = processingEnv.erasedType("java.util.Map")!!
-        ignoredTypes = IGNORED_TYPES.mapNotNull { processingEnv.erasedType(it) }
+        ignoredSuperTypes = IGNORED_SUPER_TYPES.mapNotNull { processingEnv.erasedType(it) }
+        ignoredExactTypes = IGNORED_EXACT_TYPES.mapNotNull { processingEnv.erasedType(it) }
     }
 
     override fun process(
@@ -109,7 +121,7 @@
         classType: Symbol.TypeSymbol,
         parentPolicyExceptions: Set<Immutable.Policy.Exception>,
     ): Boolean {
-        if (classType.getAnnotation(Immutable.Ignore::class.java) != null) return false
+        if (isIgnored(classType)) return false
 
         val policyAnnotation = classType.getAnnotation(Immutable.Policy::class.java)
         val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty()
@@ -131,7 +143,7 @@
             .fold(false) { anyError, field ->
                 if (field.isStatic) {
                     if (!field.isPrivate) {
-                        var finalityError = !field.modifiers.contains(Modifier.FINAL)
+                        val finalityError = !field.modifiers.contains(Modifier.FINAL)
                         if (finalityError) {
                             printError(parentChain, field, MessageUtils.staticNonFinalFailure())
                         }
@@ -177,8 +189,10 @@
         val newChain = parentChain + "$classType"
 
         val hasMethodError = filteredElements
+            .asSequence()
             .filter { it.getKind() == ElementKind.METHOD }
             .map { it as Symbol.MethodSymbol }
+            .filterNot { it.isStatic }
             .filterNot { IGNORED_METHODS.contains(it.name.toString()) }
             .fold(false) { anyError, method ->
                 // Must call visitMethod first so it doesn't get short circuited by the ||
@@ -208,6 +222,14 @@
             }
         }
 
+        // Check all of the super classes, since methods in those classes are also accessible
+        (classType as? Symbol.ClassSymbol)?.run {
+            (interfaces + superclass).forEach {
+                val element = it.asElement() ?: return@forEach
+                visitClass(parentChain, seenTypesByPolicy, element, element, newPolicyExceptions)
+            }
+        }
+
         if (isRegularClass && !anyError && allowFinalClassesFinalFields &&
             !classType.modifiers.contains(Modifier.FINAL)
         ) {
@@ -301,16 +323,14 @@
         parentPolicyExceptions: Set<Immutable.Policy.Exception>,
         nonInterfaceClassFailure: () -> String = { MessageUtils.nonInterfaceReturnFailure() },
     ): Boolean {
+        if (isIgnored(symbol)) return false
+        if (isIgnored(type)) return false
         if (type.isPrimitive) return false
         if (type.isPrimitiveOrVoid) {
             printError(parentChain, symbol, MessageUtils.voidReturnFailure())
             return true
         }
 
-        if (ignoredTypes.any { processingEnv.typeUtils.isAssignable(type, it) }) {
-            return false
-        }
-
         val policyAnnotation = symbol.getAnnotation(Immutable.Policy::class.java)
         val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty()
 
@@ -357,16 +377,38 @@
         message: String,
     ) = processingEnv.messager.printMessage(
         Diagnostic.Kind.ERROR,
-        // Drop one from the parent chain so that the directly enclosing class isn't logged.
-        // It exists in the list at this point in the traversal so that further children can
-        // include the right reference.
-        parentChain.dropLast(1).joinToString() + "\n\t" + message,
+        parentChain.plus(element.simpleName).joinToString() + "\n\t " + message,
         element,
     )
 
     private fun ProcessingEnvironment.erasedType(typeName: String) =
         elementUtils.getTypeElement(typeName)?.asType()?.let(typeUtils::erasure)
 
-    private fun isIgnored(symbol: Symbol) =
-        symbol.getAnnotation(Immutable.Ignore::class.java) != null
+    private fun isIgnored(type: Type) =
+        (type.getAnnotation(Immutable.Ignore::class.java) != null)
+                || (ignoredSuperTypes.any { type.isAssignable(it) })
+                || (ignoredExactTypes.any { type.isSameType(it) })
+
+    private fun isIgnored(symbol: Symbol) = when {
+        // Anything annotated as @Ignore is always ignored
+        symbol.getAnnotation(Immutable.Ignore::class.java) != null -> true
+        // Then ignore exact types, regardless of what kind they are
+        ignoredExactTypes.any { symbol.type.isSameType(it) } -> true
+        // Then only allow methods through, since other types (fields) are usually a failure
+        symbol.getKind() != ElementKind.METHOD -> false
+        // Finally, check for any ignored super types
+        else -> ignoredSuperTypes.any { symbol.type.isAssignable(it) }
+    }
+
+    private fun TypeMirror.isAssignable(type: TypeMirror) = try {
+        processingEnv.typeUtils.isAssignable(this, type)
+    } catch (ignored: Exception) {
+        false
+    }
+
+    private fun TypeMirror.isSameType(type: TypeMirror) = try {
+        processingEnv.typeUtils.isSameType(this, type)
+    } catch (ignored: Exception) {
+        false
+    }
 }
diff --git a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
index f26357f..2f7d59a 100644
--- a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
+++ b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
@@ -90,7 +90,7 @@
 
     @Test
     fun validInterface() = test(
-        JavaFileObjects.forSourceString(
+        source = JavaFileObjects.forSourceString(
             "$PACKAGE_PREFIX.$DATA_CLASS_NAME",
             /* language=JAVA */ """
                 package $PACKAGE_PREFIX;
@@ -227,49 +227,114 @@
             nonInterfaceReturnFailure(line = 9),
             nonInterfaceReturnFailure(line = 10, index = 0),
             classNotFinalFailure(line = 13, "NonFinalClassFinalFields"),
-        ), otherErrors = listOf(
-            memberNotMethodFailure(line = 4) to FINAL_CLASSES[1],
-            memberNotMethodFailure(line = 4) to FINAL_CLASSES[3],
+        ), otherErrors = mapOf(
+            FINAL_CLASSES[1] to listOf(
+                memberNotMethodFailure(line = 4),
+            ),
+            FINAL_CLASSES[3] to listOf(
+                memberNotMethodFailure(line = 4),
+            ),
         )
     )
 
+    @Test
+    fun superClass() {
+        val superClass = JavaFileObjects.forSourceString(
+            "$PACKAGE_PREFIX.SuperClass",
+            /* language=JAVA */ """
+            package $PACKAGE_PREFIX;
+
+            import java.util.List;
+
+            public interface SuperClass {
+                InnerClass getInnerClassOne();
+
+                final class InnerClass {
+                    public String innerField;
+                }
+            }
+            """.trimIndent()
+        )
+
+        val dataClass = JavaFileObjects.forSourceString(
+            "$PACKAGE_PREFIX.$DATA_CLASS_NAME",
+            /* language=JAVA */ """
+            package $PACKAGE_PREFIX;
+
+            import java.util.List;
+
+            @Immutable
+            public interface $DATA_CLASS_NAME extends SuperClass {
+                String[] getArray();
+            }
+            """.trimIndent()
+        )
+
+        test(
+            sources = arrayOf(superClass, dataClass),
+            fileToErrors = mapOf(
+                superClass to listOf(
+                    classNotImmutableFailure(line = 5, className = "SuperClass"),
+                    nonInterfaceReturnFailure(line = 6),
+                    nonInterfaceClassFailure(8),
+                    classNotImmutableFailure(line = 8, className = "InnerClass"),
+                    memberNotMethodFailure(line = 9),
+                ),
+                dataClass to listOf(
+                    arrayFailure(line = 7),
+                )
+            )
+        )
+    }
+
     private fun test(
         source: JavaFileObject,
         errors: List<CompilationError>,
-        otherErrors: List<Pair<CompilationError, JavaFileObject>> = emptyList(),
+        otherErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(),
+    ) = test(
+        sources = arrayOf(source),
+        fileToErrors = otherErrors + (source to errors),
+    )
+
+    private fun test(
+        vararg sources: JavaFileObject,
+        fileToErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(),
     ) {
         val compilation = javac()
             .withProcessors(ImmutabilityProcessor())
-            .compile(FINAL_CLASSES + ANNOTATION + listOf(source))
-        val allErrors = otherErrors + errors.map { it to source }
-        allErrors.forEach { (error, file) ->
-            try {
-                assertThat(compilation)
-                    .hadErrorContaining(error.message)
-                    .inFile(file)
-                    .onLine(error.line)
-            } catch (e: AssertionError) {
-                // Wrap the exception so that the line number is logged
-                val wrapped = AssertionError("Expected $error, ${e.message}").apply {
-                    stackTrace = e.stackTrace
-                }
+            .compile(FINAL_CLASSES + ANNOTATION + sources)
 
-                // Wrap again with Expect so that all errors are reported. This is very bad code
-                // but can only be fixed by updating compile-testing with a better Truth Subject
-                // implementation.
-                expect.that(wrapped).isNull()
+        fileToErrors.forEach { (file, errors) ->
+            errors.forEach { error ->
+                try {
+                    assertThat(compilation)
+                        .hadErrorContaining(error.message)
+                        .inFile(file)
+                        .onLine(error.line)
+                } catch (e: AssertionError) {
+                    // Wrap the exception so that the line number is logged
+                    val wrapped = AssertionError("Expected $error, ${e.message}").apply {
+                        stackTrace = e.stackTrace
+                    }
+
+                    // Wrap again with Expect so that all errors are reported. This is very bad code
+                    // but can only be fixed by updating compile-testing with a better Truth Subject
+                    // implementation.
+                    expect.that(wrapped).isNull()
+                }
             }
         }
 
-        try {
-            assertThat(compilation).hadErrorCount(allErrors.size)
-        } catch (e: AssertionError) {
+        expect.that(compilation.errors().size).isEqualTo(fileToErrors.values.sumOf { it.size })
+
+        if (expect.hasFailures()) {
             expect.withMessage(
                 compilation.errors()
+                    .sortedBy { it.lineNumber }
                     .joinToString(separator = "\n") {
                         "${it.lineNumber}: ${it.getMessage(Locale.ENGLISH)?.trim()}"
                     }
-            ).that(e).isNull()
+            ).fail()
         }
     }
 
@@ -307,4 +372,4 @@
         val line: Long,
         val message: String,
     )
-}
\ No newline at end of file
+}