Merge "Add some debug logging on user is not started properly"
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index d9fb318..358f009 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -177,7 +177,7 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static Clock sSystemClock = Clock.systemUTC();
 
     private abstract static class MySimpleClock extends Clock {
@@ -454,6 +454,10 @@
                                 runtimeUpdated = true;
                             }
                             break;
+                        case Constants.KEY_PERSIST_IN_SPLIT_FILES:
+                            mConstants.updatePersistingConstantsLocked();
+                            mJobs.setUseSplitFiles(mConstants.PERSIST_IN_SPLIT_FILES);
+                            break;
                         default:
                             if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY)
                                     && !concurrencyUpdated) {
@@ -537,6 +541,8 @@
         private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
                 "runtime_min_high_priority_guarantee_ms";
 
+        private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files";
+
         private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
         private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
         private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
@@ -563,6 +569,7 @@
         public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS;
         @VisibleForTesting
         static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS;
+        static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = false;
         private static final boolean DEFAULT_USE_TARE_POLICY = false;
 
         /**
@@ -678,6 +685,12 @@
                 DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
 
         /**
+         * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be
+         * saved in a single file.
+         */
+        public boolean PERSIST_IN_SPLIT_FILES = DEFAULT_PERSIST_IN_SPLIT_FILES;
+
+        /**
          * If true, use TARE policy for job limiting. If false, use quotas.
          */
         public boolean USE_TARE_POLICY = DEFAULT_USE_TARE_POLICY;
@@ -735,6 +748,11 @@
                     DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC);
         }
 
+        private void updatePersistingConstantsLocked() {
+            PERSIST_IN_SPLIT_FILES = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    KEY_PERSIST_IN_SPLIT_FILES, DEFAULT_PERSIST_IN_SPLIT_FILES);
+        }
+
         private void updatePrefetchConstantsLocked() {
             PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = DeviceConfig.getLong(
                     DeviceConfig.NAMESPACE_JOB_SCHEDULER,
@@ -835,6 +853,8 @@
             pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
                     .println();
 
+            pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println();
+
             pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY).println();
 
             pw.decreaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index 68cb049..2f94705f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -40,6 +40,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SystemConfigFileCommitEventLogger;
 import android.util.Xml;
 
@@ -89,6 +90,8 @@
 
     /** Threshold to adjust how often we want to write to the db. */
     private static final long JOB_PERSIST_DELAY = 2000L;
+    private static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
+    private static final int ALL_UIDS = -1;
 
     final Object mLock;
     final Object mWriteScheduleLock;    // used solely for invariants around write scheduling
@@ -105,13 +108,20 @@
     @GuardedBy("mWriteScheduleLock")
     private boolean mWriteInProgress;
 
+    @GuardedBy("mWriteScheduleLock")
+    private boolean mSplitFileMigrationNeeded;
+
     private static final Object sSingletonLock = new Object();
     private final SystemConfigFileCommitEventLogger mEventLogger;
     private final AtomicFile mJobsFile;
+    private final File mJobFileDirectory;
+    private final SparseBooleanArray mPendingJobWriteUids = new SparseBooleanArray();
     /** Handler backed by IoThread for writing to disk. */
     private final Handler mIoHandler = IoThread.getHandler();
     private static JobStore sSingleton;
 
+    private boolean mUseSplitFiles = JobSchedulerService.Constants.DEFAULT_PERSIST_IN_SPLIT_FILES;
+
     private JobStorePersistStats mPersistInfo = new JobStorePersistStats();
 
     /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
@@ -144,10 +154,10 @@
         mContext = context;
 
         File systemDir = new File(dataDir, "system");
-        File jobDir = new File(systemDir, "job");
-        jobDir.mkdirs();
+        mJobFileDirectory = new File(systemDir, "job");
+        mJobFileDirectory.mkdirs();
         mEventLogger = new SystemConfigFileCommitEventLogger("jobs");
-        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), mEventLogger);
+        mJobsFile = createJobFile(new File(mJobFileDirectory, "jobs.xml"));
 
         mJobSet = new JobSet();
 
@@ -162,12 +172,21 @@
         // an incorrect historical timestamp.  That's fine; at worst we'll reboot with
         // a *correct* timestamp, see a bunch of overdue jobs, and run them; then
         // settle into normal operation.
-        mXmlTimestamp = mJobsFile.getLastModifiedTime();
+        mXmlTimestamp = mJobsFile.exists()
+                ? mJobsFile.getLastModifiedTime() : mJobFileDirectory.lastModified();
         mRtcGood = (sSystemClock.millis() > mXmlTimestamp);
 
         readJobMapFromDisk(mJobSet, mRtcGood);
     }
 
+    private AtomicFile createJobFile(String baseName) {
+        return createJobFile(new File(mJobFileDirectory, baseName + ".xml"));
+    }
+
+    private AtomicFile createJobFile(File file) {
+        return new AtomicFile(file, mEventLogger);
+    }
+
     public boolean jobTimesInflatedValid() {
         return mRtcGood;
     }
@@ -211,6 +230,7 @@
     public void add(JobStatus jobStatus) {
         mJobSet.add(jobStatus);
         if (jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
             maybeWriteStatusToDiskAsync();
         }
         if (DEBUG) {
@@ -224,6 +244,9 @@
     @VisibleForTesting
     public void addForTesting(JobStatus jobStatus) {
         mJobSet.add(jobStatus);
+        if (jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
+        }
     }
 
     boolean containsJob(JobStatus jobStatus) {
@@ -257,12 +280,24 @@
             return false;
         }
         if (removeFromPersisted && jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
             maybeWriteStatusToDiskAsync();
         }
         return removed;
     }
 
     /**
+     * Like {@link #remove(JobStatus, boolean)}, but doesn't schedule a disk write.
+     */
+    @VisibleForTesting
+    public void removeForTesting(JobStatus jobStatus) {
+        mJobSet.remove(jobStatus);
+        if (jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
+        }
+    }
+
+    /**
      * Remove the jobs of users not specified in the keepUserIds.
      * @param keepUserIds Array of User IDs whose jobs should be kept and not removed.
      */
@@ -273,6 +308,7 @@
     @VisibleForTesting
     public void clear() {
         mJobSet.clear();
+        mPendingJobWriteUids.put(ALL_UIDS, true);
         maybeWriteStatusToDiskAsync();
     }
 
@@ -282,6 +318,36 @@
     @VisibleForTesting
     public void clearForTesting() {
         mJobSet.clear();
+        mPendingJobWriteUids.put(ALL_UIDS, true);
+    }
+
+    void setUseSplitFiles(boolean useSplitFiles) {
+        synchronized (mLock) {
+            if (mUseSplitFiles != useSplitFiles) {
+                mUseSplitFiles = useSplitFiles;
+                migrateJobFilesAsync();
+            }
+        }
+    }
+
+    /**
+     * The same as above but does not schedule writing. This makes perf benchmarks more stable.
+     */
+    @VisibleForTesting
+    public void setUseSplitFilesForTesting(boolean useSplitFiles) {
+        final boolean changed;
+        synchronized (mLock) {
+            changed = mUseSplitFiles != useSplitFiles;
+            if (changed) {
+                mUseSplitFiles = useSplitFiles;
+                mPendingJobWriteUids.put(ALL_UIDS, true);
+            }
+        }
+        if (changed) {
+            synchronized (mWriteScheduleLock) {
+                mSplitFileMigrationNeeded = true;
+            }
+        }
     }
 
     /**
@@ -352,6 +418,16 @@
     private static final String XML_TAG_ONEOFF = "one-off";
     private static final String XML_TAG_EXTRAS = "extras";
 
+    private void migrateJobFilesAsync() {
+        synchronized (mLock) {
+            mPendingJobWriteUids.put(ALL_UIDS, true);
+        }
+        synchronized (mWriteScheduleLock) {
+            mSplitFileMigrationNeeded = true;
+            maybeWriteStatusToDiskAsync();
+        }
+    }
+
     /**
      * Every time the state changes we write all the jobs in one swath, instead of trying to
      * track incremental changes.
@@ -449,10 +525,38 @@
      * NOTE: This Runnable locks on mLock
      */
     private final Runnable mWriteRunnable = new Runnable() {
+        private final SparseArray<AtomicFile> mJobFiles = new SparseArray<>();
+        private final CopyConsumer mPersistedJobCopier = new CopyConsumer();
+
+        class CopyConsumer implements Consumer<JobStatus> {
+            private final SparseArray<List<JobStatus>> mJobStoreCopy = new SparseArray<>();
+            private boolean mCopyAllJobs;
+
+            private void prepare() {
+                mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS);
+            }
+
+            @Override
+            public void accept(JobStatus jobStatus) {
+                final int uid = mUseSplitFiles ? jobStatus.getUid() : ALL_UIDS;
+                if (jobStatus.isPersisted() && (mCopyAllJobs || mPendingJobWriteUids.get(uid))) {
+                    List<JobStatus> uidJobList = mJobStoreCopy.get(uid);
+                    if (uidJobList == null) {
+                        uidJobList = new ArrayList<>();
+                        mJobStoreCopy.put(uid, uidJobList);
+                    }
+                    uidJobList.add(new JobStatus(jobStatus));
+                }
+            }
+
+            private void reset() {
+                mJobStoreCopy.clear();
+            }
+        }
+
         @Override
         public void run() {
             final long startElapsed = sElapsedRealtimeClock.millis();
-            final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
             // Intentionally allow new scheduling of a write operation *before* we clone
             // the job set.  If we reset it to false after cloning, there's a window in
             // which no new write will be scheduled but mLock is not held, i.e. a new
@@ -469,31 +573,73 @@
                 }
                 mWriteInProgress = true;
             }
+            final boolean useSplitFiles;
             synchronized (mLock) {
                 // Clone the jobs so we can release the lock before writing.
-                mJobSet.forEachJob(null, (job) -> {
-                    if (job.isPersisted()) {
-                        storeCopy.add(new JobStatus(job));
-                    }
-                });
+                useSplitFiles = mUseSplitFiles;
+                mPersistedJobCopier.prepare();
+                mJobSet.forEachJob(null, mPersistedJobCopier);
+                mPendingJobWriteUids.clear();
             }
-            writeJobsMapImpl(storeCopy);
+            mPersistInfo.countAllJobsSaved = 0;
+            mPersistInfo.countSystemServerJobsSaved = 0;
+            mPersistInfo.countSystemSyncManagerJobsSaved = 0;
+            for (int i = mPersistedJobCopier.mJobStoreCopy.size() - 1; i >= 0; --i) {
+                AtomicFile file;
+                if (useSplitFiles) {
+                    final int uid = mPersistedJobCopier.mJobStoreCopy.keyAt(i);
+                    file = mJobFiles.get(uid);
+                    if (file == null) {
+                        file = createJobFile(JOB_FILE_SPLIT_PREFIX + uid);
+                        mJobFiles.put(uid, file);
+                    }
+                } else {
+                    file = mJobsFile;
+                }
+                if (DEBUG) {
+                    Slog.d(TAG, "Writing for " + mPersistedJobCopier.mJobStoreCopy.keyAt(i)
+                            + " to " + file.getBaseFile().getName() + ": "
+                            + mPersistedJobCopier.mJobStoreCopy.valueAt(i).size() + " jobs");
+                }
+                writeJobsMapImpl(file, mPersistedJobCopier.mJobStoreCopy.valueAt(i));
+            }
             if (DEBUG) {
                 Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis()
                         - startElapsed) + "ms");
             }
+            mPersistedJobCopier.reset();
+            if (!useSplitFiles) {
+                mJobFiles.clear();
+            }
+            // Update the last modified time of the directory to aid in RTC time verification
+            // (see the JobStore constructor).
+            mJobFileDirectory.setLastModified(sSystemClock.millis());
             synchronized (mWriteScheduleLock) {
+                if (mSplitFileMigrationNeeded) {
+                    final File[] files = mJobFileDirectory.listFiles();
+                    for (File file : files) {
+                        if (useSplitFiles) {
+                            if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                                // Delete the now unused file so there's no confusion in the future.
+                                file.delete();
+                            }
+                        } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                            // Delete the now unused file so there's no confusion in the future.
+                            file.delete();
+                        }
+                    }
+                }
                 mWriteInProgress = false;
                 mWriteScheduleLock.notifyAll();
             }
         }
 
-        private void writeJobsMapImpl(List<JobStatus> jobList) {
+        private void writeJobsMapImpl(@NonNull AtomicFile file, @NonNull List<JobStatus> jobList) {
             int numJobs = 0;
             int numSystemJobs = 0;
             int numSyncJobs = 0;
             mEventLogger.setStartTime(SystemClock.uptimeMillis());
-            try (FileOutputStream fos = mJobsFile.startWrite()) {
+            try (FileOutputStream fos = file.startWrite()) {
                 TypedXmlSerializer out = Xml.resolveSerializer(fos);
                 out.startDocument(null, true);
                 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
@@ -523,7 +669,7 @@
                 out.endTag(null, "job-info");
                 out.endDocument();
 
-                mJobsFile.finishWrite(fos);
+                file.finishWrite(fos);
             } catch (IOException e) {
                 if (DEBUG) {
                     Slog.v(TAG, "Error writing out job data.", e);
@@ -533,9 +679,9 @@
                     Slog.d(TAG, "Error persisting bundle.", e);
                 }
             } finally {
-                mPersistInfo.countAllJobsSaved = numJobs;
-                mPersistInfo.countSystemServerJobsSaved = numSystemJobs;
-                mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs;
+                mPersistInfo.countAllJobsSaved += numJobs;
+                mPersistInfo.countSystemServerJobsSaved += numSystemJobs;
+                mPersistInfo.countSystemSyncManagerJobsSaved += numSyncJobs;
             }
         }
 
@@ -720,49 +866,82 @@
 
         @Override
         public void run() {
+            if (!mJobFileDirectory.isDirectory()) {
+                Slog.wtf(TAG, "jobs directory isn't a directory O.O");
+                mJobFileDirectory.mkdirs();
+                return;
+            }
+
             int numJobs = 0;
             int numSystemJobs = 0;
             int numSyncJobs = 0;
             List<JobStatus> jobs;
-            try (FileInputStream fis = mJobsFile.openRead()) {
-                synchronized (mLock) {
-                    jobs = readJobMapImpl(fis, rtcGood);
-                    if (jobs != null) {
-                        long now = sElapsedRealtimeClock.millis();
-                        for (int i=0; i<jobs.size(); i++) {
-                            JobStatus js = jobs.get(i);
-                            js.prepareLocked();
-                            js.enqueueTime = now;
-                            this.jobSet.add(js);
+            final File[] files;
+            try {
+                files = mJobFileDirectory.listFiles();
+            } catch (SecurityException e) {
+                Slog.wtf(TAG, "Not allowed to read job file directory", e);
+                return;
+            }
+            if (files == null) {
+                Slog.wtfStack(TAG, "Couldn't get job file list");
+                return;
+            }
+            boolean needFileMigration = false;
+            long now = sElapsedRealtimeClock.millis();
+            for (File file : files) {
+                final AtomicFile aFile = createJobFile(file);
+                try (FileInputStream fis = aFile.openRead()) {
+                    synchronized (mLock) {
+                        jobs = readJobMapImpl(fis, rtcGood);
+                        if (jobs != null) {
+                            for (int i = 0; i < jobs.size(); i++) {
+                                JobStatus js = jobs.get(i);
+                                js.prepareLocked();
+                                js.enqueueTime = now;
+                                this.jobSet.add(js);
 
-                            numJobs++;
-                            if (js.getUid() == Process.SYSTEM_UID) {
-                                numSystemJobs++;
-                                if (isSyncJob(js)) {
-                                    numSyncJobs++;
+                                numJobs++;
+                                if (js.getUid() == Process.SYSTEM_UID) {
+                                    numSystemJobs++;
+                                    if (isSyncJob(js)) {
+                                        numSyncJobs++;
+                                    }
                                 }
                             }
                         }
                     }
+                } catch (FileNotFoundException e) {
+                    // mJobFileDirectory.listFiles() gave us this file...why can't we find it???
+                    Slog.e(TAG, "Could not find jobs file: " + file.getName());
+                } catch (XmlPullParserException | IOException e) {
+                    Slog.wtf(TAG, "Error in " + file.getName(), e);
+                } catch (Exception e) {
+                    // Crashing at this point would result in a boot loop, so live with a general
+                    // Exception for system stability's sake.
+                    Slog.wtf(TAG, "Unexpected exception", e);
                 }
-            } catch (FileNotFoundException e) {
-                if (DEBUG) {
-                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
-                }
-            } catch (XmlPullParserException | IOException e) {
-                Slog.wtf(TAG, "Error jobstore xml.", e);
-            } catch (Exception e) {
-                // Crashing at this point would result in a boot loop, so live with a general
-                // Exception for system stability's sake.
-                Slog.wtf(TAG, "Unexpected exception", e);
-            } finally {
-                if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
-                    mPersistInfo.countAllJobsLoaded = numJobs;
-                    mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
-                    mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs;
+                if (mUseSplitFiles) {
+                    if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                        // We're supposed to be using the split file architecture, but we still have
+                        // the old job file around. Fully migrate and remove the old file.
+                        needFileMigration = true;
+                    }
+                } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                    // We're supposed to be using the legacy single file architecture, but we still
+                    // have some job split files around. Fully migrate and remove the split files.
+                    needFileMigration = true;
                 }
             }
+            if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
+                mPersistInfo.countAllJobsLoaded = numJobs;
+                mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
+                mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs;
+            }
             Slog.i(TAG, "Read " + numJobs + " jobs");
+            if (needFileMigration) {
+                migrateJobFilesAsync();
+            }
         }
 
         private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood)
diff --git a/core/api/current.txt b/core/api/current.txt
index 756c09f..3766a0b9 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -32542,7 +32542,7 @@
     method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.QUERY_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle);
     method public android.os.Bundle getUserRestrictions();
     method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle);
-    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.List<android.os.UserHandle> getVisibleUsers();
+    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers();
     method public boolean hasUserRestriction(String);
     method public boolean isDemoUser();
     method public static boolean isHeadlessSystemUserMode();
diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java
index 6330661..1929a4d 100644
--- a/core/java/android/os/BinderProxy.java
+++ b/core/java/android/os/BinderProxy.java
@@ -536,8 +536,8 @@
             mWarnOnBlocking = false;
             warnOnBlocking = false;
 
-            if (Build.IS_USERDEBUG) {
-                // Log this as a WTF on userdebug builds.
+            if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                // Log this as a WTF on userdebug and eng builds.
                 Log.wtf(Binder.TAG,
                         "Outgoing transactions from this process must be FLAG_ONEWAY",
                         new Throwable());
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 8eaa5ad..a887f2a 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -130,7 +130,7 @@
     boolean isUserRunning(int userId);
     boolean isUserForeground(int userId);
     boolean isUserVisible(int userId);
-    List<UserHandle> getVisibleUsers();
+    int[] getVisibleUsers();
     boolean isUserNameSet(int userId);
     boolean hasRestrictedProfiles(int userId);
     boolean requestQuietModeEnabled(String callingPackage, boolean enableQuietMode, int userId, in IntentSender target, int flags);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 3d20d63..a38d9da 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2904,12 +2904,19 @@
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS})
-    public @NonNull List<UserHandle> getVisibleUsers() {
+    public @NonNull Set<UserHandle> getVisibleUsers() {
+        ArraySet<UserHandle> result = new ArraySet<>();
         try {
-            return mService.getVisibleUsers();
+            int[] visibleUserIds = mService.getVisibleUsers();
+            if (visibleUserIds != null) {
+                for (int userId : visibleUserIds) {
+                    result.add(UserHandle.of(userId));
+                }
+            }
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
+        return result;
     }
 
     /**
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index d9ca16e..0798110 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -19,6 +19,7 @@
 #define LOG_NDEBUG 1
 
 #include <android-base/macros.h>
+#include <android-base/parsebool.h>
 #include <android-base/properties.h>
 #include <android/graphics/jni_runtime.h>
 #include <android_runtime/AndroidRuntime.h>
@@ -52,6 +53,8 @@
 using namespace android;
 using android::base::GetBoolProperty;
 using android::base::GetProperty;
+using android::base::ParseBool;
+using android::base::ParseBoolResult;
 
 extern int register_android_os_Binder(JNIEnv* env);
 extern int register_android_os_Process(JNIEnv* env);
@@ -703,17 +706,24 @@
 
     // Read if we are using the profile configuration, do this at the start since the last ART args
     // take precedence.
-    property_get("dalvik.vm.profilebootclasspath", propBuf, "");
-    std::string profile_boot_class_path_flag = propBuf;
-    // Empty means the property is unset and we should default to the phenotype property.
-    // The possible values are {"true", "false", ""}
-    if (profile_boot_class_path_flag.empty()) {
-        profile_boot_class_path_flag = server_configurable_flags::GetServerConfigurableFlag(
-                RUNTIME_NATIVE_BOOT_NAMESPACE,
-                PROFILE_BOOT_CLASS_PATH,
-                /*default_value=*/ "");
+    std::string profile_boot_class_path_flag =
+            server_configurable_flags::GetServerConfigurableFlag(RUNTIME_NATIVE_BOOT_NAMESPACE,
+                                                                 PROFILE_BOOT_CLASS_PATH,
+                                                                 /*default_value=*/"");
+    bool profile_boot_class_path;
+    switch (ParseBool(profile_boot_class_path_flag)) {
+        case ParseBoolResult::kError:
+            // Default to the system property.
+            profile_boot_class_path =
+                    GetBoolProperty("dalvik.vm.profilebootclasspath", /*default_value=*/false);
+            break;
+        case ParseBoolResult::kTrue:
+            profile_boot_class_path = true;
+            break;
+        case ParseBoolResult::kFalse:
+            profile_boot_class_path = false;
+            break;
     }
-    const bool profile_boot_class_path = (profile_boot_class_path_flag == "true");
     if (profile_boot_class_path) {
         addOption("-Xcompiler-option");
         addOption("--count-hotness-in-compiled-code");
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index f1a24af..50cab84 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -35,8 +35,8 @@
         </activity>
 
         <provider
-            android:name=".GalleryEntryProvider"
-            android:authorities="com.android.spa.gallery.provider"
+            android:name="com.android.settingslib.spa.framework.SpaSearchProvider"
+            android:authorities="com.android.spa.gallery.search.provider"
             android:enabled="true"
             android:exported="false">
         </provider>
@@ -51,7 +51,7 @@
         </activity>
         <provider
             android:name="com.android.settingslib.spa.framework.debug.DebugProvider"
-            android:authorities="com.android.spa.gallery.debug"
+            android:authorities="com.android.spa.gallery.debug.provider"
             android:enabled="true"
             android:exported="false">
         </provider>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
deleted file mode 100644
index 817c209f..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery
-
-import com.android.settingslib.spa.framework.EntryProvider
-
-class GalleryEntryProvider : EntryProvider()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 742e271..016b27f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -80,7 +80,7 @@
 
     override val browseActivityClass = GalleryMainActivity::class.java
 
-    override val entryProviderAuthorities = "com.android.spa.gallery.provider"
+    override val searchProviderAuthorities = "com.android.spa.gallery.search.provider"
 
     override val logger = LocalLogger()
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
similarity index 98%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
index 38f41bc..35b9c0f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
@@ -33,7 +33,7 @@
 import com.android.settingslib.spa.framework.common.addUri
 import com.android.settingslib.spa.framework.common.getColumns
 
-private const val TAG = "EntryProvider"
+private const val TAG = "SpaSearchProvider"
 
 /**
  * The content provider to return entry related data, which can be used for search and hierarchy.
@@ -47,7 +47,7 @@
  *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
  *   $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
  */
-open class EntryProvider : ContentProvider() {
+class SpaSearchProvider : ContentProvider() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
     private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index b831043..a9cb041 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -64,7 +64,7 @@
 
     open val browseActivityClass: Class<out Activity>? = null
 
-    open val entryProviderAuthorities: String? = null
+    open val searchProviderAuthorities: String? = null
 
     open val logger: SpaLogger = object : SpaLogger {}
 
diff --git a/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml
new file mode 100644
index 0000000..46abff8
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml
@@ -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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:viewportWidth="22"
+    android:viewportHeight="17"
+    android:width="22dp"
+    android:height="17dp">
+  <group>
+    <group>
+      <path android:fillColor="#FF000000"
+          android:pathData="M1.03 8.47l0.43-4.96h4.33v1.17H2.48L2.25 7.39C2.66 7.1 3.1 6.96 3.57 6.96c0.77 0 1.38 0.3 1.83 0.9 s0.66 1.41 0.66 2.43c0 1.03-0.24 1.84-0.72 2.43S4.2 13.6 3.36 13.6c-0.75 0-1.36-0.24-1.83-0.73s-0.74-1.16-0.81-2.02h1.13 c0.07 0.57 0.23 1 0.49 1.29s0.59 0.43 1.01 0.43c0.47 0 0.84-0.2 1.1-0.61c0.26-0.41 0.4-0.96 0.4-1.65 c0-0.65-0.14-1.18-0.43-1.59S3.76 8.09 3.28 8.09c-0.4 0-0.72 0.1-0.96 0.31L1.99 8.73L1.03 8.47z"/>
+    </group>
+    <group>
+      <path android:fillColor="#FF000000"
+          android:pathData="M 18.93,5.74 L 18.93,3.39 L 17.63,3.39 L 17.63,5.74 L 15.28,5.74 L 15.28,7.04 L 17.63,7.04 L 17.63,9.39 L 18.93,9.39 L 18.93,7.04 L 21.28,7.04 L 21.28,5.74 z"/>
+    </group>
+    <path android:fillColor="#FF000000"
+        android:pathData="M13.78 12.24l-0.22 0.27c-0.63 0.73-1.55 1.1-2.76 1.1c-1.08 0-1.92-0.36-2.53-1.07s-0.93-1.72-0.94-3.02V7.56 c0-1.39 0.28-2.44 0.84-3.13s1.39-1.04 2.51-1.04c0.95 0 1.69 0.26 2.23 0.79s0.83 1.28 0.89 2.26h-1.25 c-0.05-0.62-0.22-1.1-0.52-1.45s-0.74-0.52-1.34-0.52c-0.72 0-1.24 0.23-1.57 0.7S8.6 6.37 8.59 7.4v2.03c0 1 0.19 1.77 0.57 2.31 c0.38 0.54 0.93 0.8 1.65 0.8c0.67 0 1.19-0.16 1.54-0.49l0.18-0.17V9.59h-1.82V8.52h3.07V12.24z"/>
+  </group>
+</vector>
diff --git a/packages/SettingsLib/res/values/carrierid_icon_overrides.xml b/packages/SettingsLib/res/values/carrierid_icon_overrides.xml
new file mode 100644
index 0000000..d2ae52d
--- /dev/null
+++ b/packages/SettingsLib/res/values/carrierid_icon_overrides.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<!--
+  ~ This resource file exists to enumerate all network type icon overrides on a
+  ~ per-carrierId basis
+-->
+<resources>
+    <!--
+    Network type (RAT) icon overrides can be configured here on a per-carrierId basis.
+        1. Add a new TypedArray here, using the naming scheme below
+        2. The entries are (NetworkType, drawable ID) pairs
+        3. Add this array's ID to the MAPPING field of MobileIconCarrierIdOverrides.kt
+    -->
+    <array name="carrierId_2032_iconOverrides">
+        <item>5G_PLUS</item>
+        <item>@drawable/ic_5g_plus_mobiledata_default</item>
+    </array>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt
new file mode 100644
index 0000000..a0395b5
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.settingslib.mobile
+
+import android.annotation.DrawableRes
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.R
+import com.android.settingslib.SignalIcon.MobileIconGroup
+
+/**
+ * This class defines a network type (3G, 4G, etc.) override mechanism on a per-carrierId basis.
+ *
+ * Traditionally, carrier-customized network type iconography was achieved using the `MCC/MNC`
+ * resource qualifiers, and swapping out the drawable resource by name. It would look like this:
+ *
+ *     res/
+ *       drawable/
+ *         3g_mobiledata_icon.xml
+ *       drawable-MCC-MNC/
+ *         3g_mobiledata_icon.xml
+ *
+ * This would mean that, provided a context created with this MCC/MNC configuration set, loading
+ * the network type icon through [MobileIconGroup] would provide a carrier-defined network type
+ * icon rather than the AOSP-defined default.
+ *
+ * The MCC/MNC mechanism no longer can fully define carrier-specific network type icons, because
+ * there is no longer a 1:1 mapping between MCC/MNC and carrier. With the advent of MVNOs, multiple
+ * carriers can have the same MCC/MNC value, but wish to differentiate based on their carrier ID.
+ * CarrierId is a newer concept than MCC/MNC, and provides more granularity when it comes to
+ * determining the carrier (e.g. MVNOs can share MCC/MNC values with the network owner), therefore
+ * it can fit all of the same use cases currently handled by `MCC/MNC`, without the need to apply a
+ * configuration context in order to get the proper UI for a given SIM icon.
+ *
+ * NOTE: CarrierId icon overrides will always take precedence over those defined using `MCC/MNC`
+ * resource qualifiers.
+ *
+ * [MAPPING] encodes the relationship between CarrierId and the corresponding override array
+ * that exists in the config.xml. An alternative approach could be to generate the resource name
+ * by string concatenation at run-time:
+ *
+ *    val resName = "carrierId_$carrierId_iconOverrides"
+ *    val override = resources.getResourceIdentifier(resName)
+ *
+ * However, that's going to be far less efficient until MAPPING grows to a sufficient size. For now,
+ * given a relatively small number of entries, we should just maintain the mapping here.
+ */
+interface MobileIconCarrierIdOverrides {
+    @DrawableRes
+    fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int
+    fun carrierIdEntryExists(carrierId: Int): Boolean
+}
+
+class MobileIconCarrierIdOverridesImpl : MobileIconCarrierIdOverrides {
+    @DrawableRes
+    override fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int {
+        val resId = MAPPING[carrierId] ?: return 0
+        val ta = resources.obtainTypedArray(resId)
+        val map = parseNetworkIconOverrideTypedArray(ta)
+        ta.recycle()
+        return map[networkType] ?: 0
+    }
+
+    override fun carrierIdEntryExists(carrierId: Int) =
+        overrideExists(carrierId, MAPPING)
+
+    companion object {
+        private const val TAG = "MobileIconOverrides"
+        /**
+         * This map maintains the lookup from the canonical carrier ID (see below link) to the
+         * corresponding overlay resource. New overrides should add an entry below in order to
+         * change the network type icon resources based on carrier ID
+         *
+         * Refer to the link below for the canonical mapping maintained in AOSP:
+         * https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/latest_carrier_id/carrier_list.textpb
+         */
+        private val MAPPING = mapOf(
+            // 2032 == Xfinity Mobile
+            2032 to R.array.carrierId_2032_iconOverrides,
+        )
+
+        /**
+         * Parse `carrierId_XXXX_iconOverrides` for a particular network type. The resource file
+         * "carrierid_icon_overrides.xml" defines a TypedArray format for overriding specific
+         * network type icons (a.k.a. RAT icons) for a particular carrier ID. The format is defined
+         * as an array of (network type name, drawable) pairs:
+         *    <array name="carrierId_XXXX_iconOverrides>
+         *        <item>NET_TYPE_1</item>
+         *        <item>@drawable/net_type_1_override</item>
+         *        <item>NET_TYPE_2</item>
+         *        <item>@drawable/net_type_2_override</item>
+         *    </array>
+         *
+         * @param ta the [TypedArray] defined in carrierid_icon_overrides.xml
+         * @return the overridden drawable resource ID if it exists, or 0 if it does not
+         */
+        @VisibleForTesting
+        @JvmStatic
+        fun parseNetworkIconOverrideTypedArray(ta: TypedArray): Map<String, Int> {
+            if (ta.length() % 2 != 0) {
+                Log.w(TAG,
+                    "override must contain an even number of (key, value) entries. skipping")
+
+                return mapOf()
+            }
+
+            val result = mutableMapOf<String, Int>()
+            // The array is defined as Pair(String, resourceId), so walk by 2
+            for (i in 0 until ta.length() step 2) {
+                val key = ta.getString(i)
+                val override = ta.getResourceId(i + 1, 0)
+                if (key == null || override == 0) {
+                    Log.w(TAG, "Invalid override found. Skipping")
+                    continue
+                }
+                result[key] = override
+            }
+
+            return result
+        }
+
+        @JvmStatic
+        private fun overrideExists(carrierId: Int, mapping: Map<Int, Int>): Boolean =
+            mapping.containsKey(carrierId)
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java
new file mode 100644
index 0000000..740261d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.settingslib.mobile;
+
+import static com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl.parseNetworkIconOverrideTypedArray;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.res.TypedArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class MobileIconCarrierIdOverridesTest {
+    private static final String OVERRIDE_ICON_1_NAME = "name_1";
+    private static final int OVERRIDE_ICON_1_RES = 1;
+
+    private static final String OVERRIDE_ICON_2_NAME = "name_2";
+    private static final int OVERRIDE_ICON_2_RES = 2;
+
+    NetworkOverrideTypedArrayMock mResourceMock;
+
+    @Before
+    public void setUp() {
+        mResourceMock = new NetworkOverrideTypedArrayMock(
+                new String[] { OVERRIDE_ICON_1_NAME, OVERRIDE_ICON_2_NAME },
+                new int[] { OVERRIDE_ICON_1_RES, OVERRIDE_ICON_2_RES }
+        );
+    }
+
+    @Test
+    public void testParse_singleOverride() {
+        mResourceMock.setOverrides(
+                new String[] { OVERRIDE_ICON_1_NAME },
+                new int[] { OVERRIDE_ICON_1_RES }
+        );
+
+        Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock());
+
+        assertThat(parsed.get(OVERRIDE_ICON_1_NAME)).isEqualTo(OVERRIDE_ICON_1_RES);
+    }
+
+    @Test
+    public void testParse_multipleOverrides() {
+        mResourceMock.setOverrides(
+                new String[] { OVERRIDE_ICON_1_NAME, OVERRIDE_ICON_2_NAME },
+                new int[] { OVERRIDE_ICON_1_RES, OVERRIDE_ICON_2_RES }
+        );
+
+        Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock());
+
+        assertThat(parsed.get(OVERRIDE_ICON_2_NAME)).isEqualTo(OVERRIDE_ICON_2_RES);
+        assertThat(parsed.get(OVERRIDE_ICON_1_NAME)).isEqualTo(OVERRIDE_ICON_1_RES);
+    }
+
+    @Test
+    public void testParse_nonexistentKey_isNull() {
+        mResourceMock.setOverrides(
+                new String[] { OVERRIDE_ICON_1_NAME },
+                new int[] { OVERRIDE_ICON_1_RES }
+        );
+
+        Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock());
+
+        assertThat(parsed.get(OVERRIDE_ICON_2_NAME)).isNull();
+    }
+
+    static class NetworkOverrideTypedArrayMock {
+        private Object[] mInterleaved;
+
+        private final TypedArray mMockTypedArray = mock(TypedArray.class);
+
+        NetworkOverrideTypedArrayMock(
+                String[] networkTypes,
+                int[] iconOverrides) {
+
+            mInterleaved = interleaveTypes(networkTypes, iconOverrides);
+
+            doAnswer(invocation -> {
+                return mInterleaved[(int) invocation.getArgument(0)];
+            }).when(mMockTypedArray).getString(/* index */ anyInt());
+
+            doAnswer(invocation -> {
+                return mInterleaved[(int) invocation.getArgument(0)];
+            }).when(mMockTypedArray).getResourceId(/* index */ anyInt(), /* default */ anyInt());
+
+            when(mMockTypedArray.length()).thenAnswer(invocation -> {
+                return mInterleaved.length;
+            });
+        }
+
+        TypedArray getMock() {
+            return mMockTypedArray;
+        }
+
+        void setOverrides(String[] types, int[] resIds) {
+            mInterleaved = interleaveTypes(types, resIds);
+        }
+
+        private Object[] interleaveTypes(String[] strs, int[] ints) {
+            assertThat(strs.length).isEqualTo(ints.length);
+
+            Object[] ret = new Object[strs.length * 2];
+
+            // Keep track of where we are in the interleaved array, but iterate the overrides
+            int interleavedIndex = 0;
+            for (int i = 0; i < strs.length; i++) {
+                ret[interleavedIndex] = strs[i];
+                interleavedIndex += 1;
+                ret[interleavedIndex] = ints[i];
+                interleavedIndex += 1;
+            }
+            return ret;
+        }
+    }
+}
diff --git a/packages/SystemUI/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml
index a4e7a5f..f1aa544 100644
--- a/packages/SystemUI/res-keyguard/values-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml
@@ -27,4 +27,6 @@
     <integer name="scaled_password_text_size">26</integer>
 
     <dimen name="bouncer_user_switcher_y_trans">@dimen/status_bar_height</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 0a55cf7..3861d98 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -124,6 +124,8 @@
     <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen>
     <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen>
     <dimen name="bouncer_user_switcher_y_trans">0dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
 
     <!-- 2 * the margin + size should equal the plus_margin -->
     <dimen name="user_switcher_icon_large_margin">16dp</dimen>
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
index a936914..d6c9e98 100644
--- a/packages/SystemUI/res/layout/screen_record_options.xml
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -50,6 +50,7 @@
             android:importantForAccessibility="yes"/>
     </LinearLayout>
     <LinearLayout
+        android:id="@+id/show_taps"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
diff --git a/packages/SystemUI/res/values-sw600dp-port/dimens.xml b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
index d9df337..707bc9e 100644
--- a/packages/SystemUI/res/values-sw600dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
@@ -17,7 +17,6 @@
 <resources>
     <dimen name="notification_panel_margin_horizontal">48dp</dimen>
     <dimen name="status_view_margin_horizontal">62dp</dimen>
-    <dimen name="bouncer_user_switcher_y_trans">20dp</dimen>
 
     <!-- qs_tiles_page_horizontal_margin should be margin / 2, otherwise full space between two
          pages is margin * 2, and that makes tiles page not appear immediately after user swipes to
diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
index 97ead01..b98165f 100644
--- a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
@@ -21,4 +21,6 @@
     <!-- Space between status view and notification shelf -->
     <dimen name="keyguard_status_view_bottom_margin">70dp</dimen>
     <dimen name="keyguard_clock_top_margin">80dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">186dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">110dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
index 17f82b5..8b41a44 100644
--- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
@@ -21,7 +21,6 @@
      for different hardware and product builds. -->
 <resources>
     <dimen name="status_view_margin_horizontal">124dp</dimen>
-    <dimen name="bouncer_user_switcher_y_trans">200dp</dimen>
 
     <dimen name="large_screen_shade_header_left_padding">24dp</dimen>
     <dimen name="qqs_layout_padding_bottom">40dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0e48760..6824d7f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -405,7 +405,7 @@
     <string name="keyguard_face_failed">Can\u2019t recognize face</string>
     <!-- Message shown to suggest using fingerprint sensor to authenticate after another biometric failed. [CHAR LIMIT=25] -->
     <string name="keyguard_suggest_fingerprint">Use fingerprint instead</string>
-    <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=25] -->
+    <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=65] -->
     <string name="keyguard_face_unlock_unavailable">Face unlock unavailable.</string>
 
     <!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
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
index 40c8774..a790d89 100644
--- 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
@@ -61,26 +61,42 @@
      * 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 canvasWidth, int canvasHeight, int screenWidthPx, int screenHeightPx,
+            int taskbarSize, boolean isTablet,
             int currentRotation, boolean isRtl) {
         boolean isRotated = false;
         boolean isOrientationDifferent;
 
-        float fullscreenTaskWidth = screenWidthPx;
-        if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) {
-            // For landscape, scale the width
-            float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
-                    ? mSplitBounds.leftTaskPercent
-                    : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
-            // Scale landscape width to that of actual screen
-            fullscreenTaskWidth = screenWidthPx * taskPercent;
-        }
         int thumbnailRotation = thumbnailData.rotation;
         int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
         RectF thumbnailClipHint = new RectF();
-        float canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
-        float scaledTaskbarSize = taskbarSize * canvasScreenRatio;
-        thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
+
+        float scaledTaskbarSize = 0;
+        if (mSplitBounds != null) {
+            float fullscreenTaskWidth;
+            float fullscreenTaskHeight;
+            float canvasScreenRatio;
+
+            float taskPercent;
+            if (!mSplitBounds.appsStackedVertically) {
+                // For landscape, scale the width
+                taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
+                        ? mSplitBounds.leftTaskPercent
+                        : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
+                // Scale landscape width to that of actual screen
+                fullscreenTaskWidth = screenWidthPx * taskPercent;
+                canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
+            } else {
+                taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT
+                        ? mSplitBounds.leftTaskPercent
+                        : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
+                // Scale landscape width to that of actual screen
+                fullscreenTaskHeight = screenHeightPx * taskPercent;
+                canvasScreenRatio = canvasHeight / fullscreenTaskHeight;
+            }
+            scaledTaskbarSize = taskbarSize * canvasScreenRatio;
+            thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
+        }
 
         float scale = thumbnailData.scale;
         final float thumbnailScale;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 73229c3..faaba63 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -21,6 +21,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.inputmethod.InputMethodManager;
 
@@ -152,7 +153,9 @@
     }
 
     public void startAppearAnimation() {
-        mMessageAreaController.setMessage(getInitialMessageResId());
+        if (TextUtils.isEmpty(mMessageAreaController.getMessage())) {
+            mMessageAreaController.setMessage(getInitialMessageResId());
+        }
         mView.startAppearAnimation();
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index 2bd3ca5..db986e0 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -103,6 +103,11 @@
         mView.setNextMessageColor(colorState);
     }
 
+    /** Returns the message of the underlying TextView. */
+    public CharSequence getMessage() {
+        return mView.getText();
+    }
+
     /**
      * Reload colors from resources.
      **/
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 2bb3a5f..ffcf42f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1097,11 +1097,19 @@
                         new KeyguardSecurityViewTransition());
             }
             int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
+            int viewFlipperBottomMargin = mResources.getDimensionPixelSize(
+                    R.dimen.bouncer_user_switcher_view_mode_view_flipper_bottom_margin);
+            int userSwitcherBottomMargin = mResources.getDimensionPixelSize(
+                    R.dimen.bouncer_user_switcher_view_mode_user_switcher_bottom_margin);
             if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                 ConstraintSet constraintSet = new ConstraintSet();
                 constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP, yTrans);
-                constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP);
-                constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM);
+                constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, mViewFlipper.getId(),
+                        TOP, userSwitcherBottomMargin);
+                constraintSet.connect(mViewFlipper.getId(), TOP, mUserSwitcherViewGroup.getId(),
+                        BOTTOM);
+                constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM,
+                        viewFlipperBottomMargin);
                 constraintSet.centerHorizontally(mViewFlipper.getId(), PARENT_ID);
                 constraintSet.centerHorizontally(mUserSwitcherViewGroup.getId(), PARENT_ID);
                 constraintSet.setVerticalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 99dfefa..002ffdb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -146,8 +146,10 @@
     // TODO(b/255607168): Tracking Bug
     @JvmField val DOZING_MIGRATION_1 = unreleasedFlag(213, "dozing_migration_1")
 
+    // TODO(b/252897742): Tracking Bug
     @JvmField val NEW_ELLIPSE_DETECTION = unreleasedFlag(214, "new_ellipse_detection")
 
+    // TODO(b/252897742): Tracking Bug
     @JvmField val NEW_UDFPS_OVERLAY = unreleasedFlag(215, "new_udfps_overlay")
 
     /**
@@ -160,6 +162,10 @@
     val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES =
         unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false)
 
+    /** Shows chipbar UI whenever the device is unlocked by ActiveUnlock (watch). */
+    // TODO(b/240196500): Tracking Bug
+    @JvmField val ACTIVE_UNLOCK_CHIPBAR = unreleasedFlag(217, "active_unlock_chipbar")
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index d4514c5..783f752 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -16,9 +16,7 @@
 
 package com.android.systemui.keyguard.data.repository
 
-import android.hardware.biometrics.BiometricSourceType
 import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.keyguard.ViewMediatorCallback
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
@@ -70,33 +68,15 @@
     private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
     /** Determines if user is already unlocked */
     val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
-
-    var bouncerPromptReason: Int? = null
     private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
     val showMessage = _showMessage.asStateFlow()
     private val _resourceUpdateRequests = MutableStateFlow(false)
     val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
-
+    val bouncerPromptReason: Int
+        get() = viewMediatorCallback.bouncerPromptReason
     val bouncerErrorMessage: CharSequence?
         get() = viewMediatorCallback.consumeCustomMessage()
 
-    init {
-        val callback =
-            object : KeyguardUpdateMonitorCallback() {
-                override fun onStrongAuthStateChanged(userId: Int) {
-                    bouncerPromptReason = viewMediatorCallback.bouncerPromptReason
-                }
-
-                override fun onLockedOutStateChanged(type: BiometricSourceType) {
-                    if (type == BiometricSourceType.FINGERPRINT) {
-                        bouncerPromptReason = viewMediatorCallback.bouncerPromptReason
-                    }
-                }
-            }
-
-        keyguardUpdateMonitor.registerCallback(callback)
-    }
-
     fun setPrimaryScrimmed(isScrimmed: Boolean) {
         _primaryBouncerScrimmed.value = isScrimmed
     }
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 ca25282..9d5d8bb 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
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.common.shared.model.Position
@@ -69,6 +70,9 @@
      */
     val isKeyguardShowing: Flow<Boolean>
 
+    /** Observable for the signal that keyguard is about to go away. */
+    val isKeyguardGoingAway: Flow<Boolean>
+
     /** Observable for whether the bouncer is showing. */
     val isBouncerShowing: Flow<Boolean>
 
@@ -85,6 +89,14 @@
     val isDozing: Flow<Boolean>
 
     /**
+     * Observable for whether the device is dreaming.
+     *
+     * Dozing/AOD is a specific type of dream, but it is also possible for other non-systemui dreams
+     * to be active, such as screensavers.
+     */
+    val isDreaming: Flow<Boolean>
+
+    /**
      * Observable for the amount of doze we are currently in.
      *
      * While in doze state, this amount can change - driving a cycle of animations designed to avoid
@@ -136,12 +148,12 @@
 class KeyguardRepositoryImpl
 @Inject
 constructor(
-        statusBarStateController: StatusBarStateController,
-        dozeHost: DozeHost,
-        wakefulnessLifecycle: WakefulnessLifecycle,
-        biometricUnlockController: BiometricUnlockController,
-        private val keyguardStateController: KeyguardStateController,
-        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    statusBarStateController: StatusBarStateController,
+    dozeHost: DozeHost,
+    wakefulnessLifecycle: WakefulnessLifecycle,
+    biometricUnlockController: BiometricUnlockController,
+    private val keyguardStateController: KeyguardStateController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) : KeyguardRepository {
     private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
     override val animateBottomAreaDozingTransitions =
@@ -176,6 +188,29 @@
         awaitClose { keyguardStateController.removeCallback(callback) }
     }
 
+    override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : KeyguardStateController.Callback {
+                override fun onKeyguardGoingAwayChanged() {
+                    trySendWithFailureLogging(
+                        keyguardStateController.isKeyguardGoingAway,
+                        TAG,
+                        "updated isKeyguardGoingAway"
+                    )
+                }
+            }
+
+        keyguardStateController.addCallback(callback)
+        // Adding the callback does not send an initial update.
+        trySendWithFailureLogging(
+            keyguardStateController.isKeyguardGoingAway,
+            TAG,
+            "initial isKeyguardGoingAway"
+        )
+
+        awaitClose { keyguardStateController.removeCallback(callback) }
+    }
+
     override val isBouncerShowing: Flow<Boolean> = conflatedCallbackFlow {
         val callback =
             object : KeyguardStateController.Callback {
@@ -218,6 +253,25 @@
             }
             .distinctUntilChanged()
 
+    override val isDreaming: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onDreamingStateChanged(isDreaming: Boolean) {
+                            trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
+                        }
+                    }
+                keyguardUpdateMonitor.registerCallback(callback)
+                trySendWithFailureLogging(
+                    keyguardUpdateMonitor.isDreaming,
+                    TAG,
+                    "initial isDreaming",
+                )
+
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+
     override val dozeAmount: Flow<Float> = conflatedCallbackFlow {
         val callback =
             object : StatusBarStateController.StateListener {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index e3d1a27..bce7d92 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -94,11 +94,13 @@
      */
     private val _transitions =
         MutableSharedFlow<TransitionStep>(
+            replay = 2,
             extraBufferCapacity = 10,
-            onBufferOverflow = BufferOverflow.DROP_OLDEST
+            onBufferOverflow = BufferOverflow.DROP_OLDEST,
         )
     override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
     private var lastStep: TransitionStep = TransitionStep()
+    private var lastAnimator: ValueAnimator? = null
 
     /*
      * When manual control of the transition is requested, a unique [UUID] is used as the handle
@@ -106,19 +108,39 @@
      */
     private var updateTransitionId: UUID? = null
 
+    init {
+        // Seed with transitions signaling a boot into lockscreen state
+        emitTransition(
+            TransitionStep(
+                KeyguardState.OFF,
+                KeyguardState.LOCKSCREEN,
+                0f,
+                TransitionState.STARTED,
+            )
+        )
+        emitTransition(
+            TransitionStep(
+                KeyguardState.OFF,
+                KeyguardState.LOCKSCREEN,
+                1f,
+                TransitionState.FINISHED,
+            )
+        )
+    }
+
     override fun startTransition(info: TransitionInfo): UUID? {
         if (lastStep.transitionState != TransitionState.FINISHED) {
-            // Open questions:
-            // * Queue of transitions? buffer of 1?
-            // * Are transitions cancellable if a new one is triggered?
-            // * What validation does this need to do?
-            Log.wtf(TAG, "Transition still active: $lastStep")
-            return null
+            Log.i(TAG, "Transition still active: $lastStep, canceling")
         }
 
+        val startingValue = 1f - lastStep.value
+        lastAnimator?.cancel()
+        lastAnimator = info.animator
+
         info.animator?.let { animator ->
             // An animator was provided, so use it to run the transition
-            animator.setFloatValues(0f, 1f)
+            animator.setFloatValues(startingValue, 1f)
+            animator.duration = ((1f - startingValue) * animator.duration).toLong()
             val updateListener =
                 object : AnimatorUpdateListener {
                     override fun onAnimationUpdate(animation: ValueAnimator) {
@@ -134,15 +156,24 @@
             val adapter =
                 object : AnimatorListenerAdapter() {
                     override fun onAnimationStart(animation: Animator) {
-                        emitTransition(TransitionStep(info, 0f, TransitionState.STARTED))
+                        emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED))
                     }
                     override fun onAnimationCancel(animation: Animator) {
-                        Log.i(TAG, "Cancelling transition: $info")
+                        endAnimation(animation, lastStep.value, TransitionState.CANCELED)
                     }
                     override fun onAnimationEnd(animation: Animator) {
-                        emitTransition(TransitionStep(info, 1f, TransitionState.FINISHED))
+                        endAnimation(animation, 1f, TransitionState.FINISHED)
+                    }
+
+                    private fun endAnimation(
+                        animation: Animator,
+                        value: Float,
+                        state: TransitionState
+                    ) {
+                        emitTransition(TransitionStep(info, value, state))
                         animator.removeListener(this)
                         animator.removeUpdateListener(updateListener)
+                        lastAnimator = null
                     }
                 }
             animator.addListener(adapter)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
index 0aeff7f..e5521c7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
@@ -20,10 +20,11 @@
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isSleepingOrStartingToSleep
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -35,18 +36,30 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
-    private val keyguardRepository: KeyguardRepository,
+    private val keyguardInteractor: KeyguardInteractor,
     private val keyguardTransitionRepository: KeyguardTransitionRepository,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) : TransitionInteractor("AOD<->LOCKSCREEN") {
 
     override fun start() {
         scope.launch {
-            keyguardRepository.isDozing
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+            /*
+             * Listening to the startedKeyguardTransitionStep (last started step) allows this code
+             * to interrupt an active transition, as long as they were either going to LOCKSCREEN or
+             * AOD state. One example is when the user presses the power button in the middle of an
+             * active transition.
+             */
+            keyguardInteractor.wakefulnessState
+                .sample(
+                    keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                    { a, b -> Pair(a, b) }
+                )
                 .collect { pair ->
-                    val (isDozing, keyguardState) = pair
-                    if (isDozing && keyguardState == KeyguardState.LOCKSCREEN) {
+                    val (wakefulnessState, lastStartedStep) = pair
+                    if (
+                        isSleepingOrStartingToSleep(wakefulnessState) &&
+                            lastStartedStep.to == KeyguardState.LOCKSCREEN
+                    ) {
                         keyguardTransitionRepository.startTransition(
                             TransitionInfo(
                                 name,
@@ -55,7 +68,10 @@
                                 getAnimator(),
                             )
                         )
-                    } else if (!isDozing && keyguardState == KeyguardState.AOD) {
+                    } else if (
+                        isWakingOrStartingToWake(wakefulnessState) &&
+                            lastStartedStep.to == KeyguardState.AOD
+                    ) {
                         keyguardTransitionRepository.startTransition(
                             TransitionInfo(
                                 name,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
new file mode 100644
index 0000000..dd29673
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class BouncerToGoneTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val shadeRepository: ShadeRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor
+) : TransitionInteractor("BOUNCER->GONE") {
+
+    private var transitionId: UUID? = null
+
+    override fun start() {
+        listenForKeyguardGoingAway()
+    }
+
+    private fun listenForKeyguardGoingAway() {
+        scope.launch {
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isKeyguardGoingAway, keyguardState) = pair
+                    if (isKeyguardGoingAway && keyguardState == KeyguardState.BOUNCER) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.BOUNCER,
+                                to = KeyguardState.GONE,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..c44cda4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class DreamingLockscreenTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("DREAMING<->LOCKSCREEN") {
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.isDreaming
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isDreaming, keyguardState) = pair
+                    if (isDreaming && keyguardState == KeyguardState.LOCKSCREEN) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.LOCKSCREEN,
+                                KeyguardState.DREAMING,
+                                getAnimator(),
+                            )
+                        )
+                    } else if (!isDreaming && keyguardState == KeyguardState.DREAMING) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.DREAMING,
+                                KeyguardState.LOCKSCREEN,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt
new file mode 100644
index 0000000..9e2b724
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isSleepingOrStartingToSleep
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class DreamingToAodTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("DREAMING->AOD") {
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.wakefulnessState
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (wakefulnessState, keyguardState) = pair
+                    if (
+                        isSleepingOrStartingToSleep(wakefulnessState) &&
+                            keyguardState == KeyguardState.DREAMING
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.DREAMING,
+                                KeyguardState.AOD,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
+}
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 614ff8d..5a1c702 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
@@ -41,8 +41,15 @@
     val dozeAmount: Flow<Float> = repository.dozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
+    /**
+     * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
+     * but not vice-versa.
+     */
+    val isDreaming: Flow<Boolean> = repository.isDreaming
     /** Whether the keyguard is showing or not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+    /** Whether the keyguard is going away. */
+    val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
     /** Whether the bouncer is showing or not. */
     val isBouncerShowing: Flow<Boolean> = repository.isBouncerShowing
     /** The device wake/sleep state */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index 57fb4a1..58a8093 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -41,12 +41,24 @@
         }
 
         scope.launch {
+            keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) }
+        }
+
+        scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } }
+
+        scope.launch {
             interactor.finishedKeyguardTransitionStep.collect {
                 logger.i("Finished transition", it)
             }
         }
 
         scope.launch {
+            interactor.canceledKeyguardTransitionStep.collect {
+                logger.i("Canceled transition", it)
+            }
+        }
+
+        scope.launch {
             interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index a7c6d44..43dd358e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -42,6 +42,9 @@
                     is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
                     is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
                     is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is DreamingLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is DreamingToAodTransitionInteractor -> Log.d(TAG, "Started $it")
                 }
             it.start()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 749183e..54a4f49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -57,6 +57,14 @@
             lockscreenToAodTransition,
         )
 
+    /* The last [TransitionStep] with a [TransitionState] of STARTED */
+    val startedKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
+
+    /* The last [TransitionStep] with a [TransitionState] of CANCELED */
+    val canceledKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.CANCELED }
+
     /* The last [TransitionStep] with a [TransitionState] of FINISHED */
     val finishedKeyguardTransitionStep: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
@@ -64,8 +72,4 @@
     /* The last completed [KeyguardState] transition */
     val finishedKeyguardState: Flow<KeyguardState> =
         finishedKeyguardTransitionStep.map { step -> step.to }
-
-    /* The last [TransitionStep] with a [TransitionState] of STARTED */
-    val startedKeyguardTransitionStep: Flow<TransitionStep> =
-        repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
index fd4814d..cca2d56 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -56,10 +56,20 @@
     private fun listenForBouncerHiding() {
         scope.launch {
             keyguardInteractor.isBouncerShowing
-                .sample(keyguardInteractor.wakefulnessState, { a, b -> Pair(a, b) })
-                .collect { pair ->
-                    val (isBouncerShowing, wakefulnessState) = pair
-                    if (!isBouncerShowing) {
+                .sample(
+                    combine(
+                        keyguardInteractor.wakefulnessState,
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                    ) { a, b ->
+                        Pair(a, b)
+                    },
+                    { a, bc -> Triple(a, bc.first, bc.second) }
+                )
+                .collect { triple ->
+                    val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
+                    if (
+                        !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
+                    ) {
                         val to =
                             if (
                                 wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP ||
@@ -90,10 +100,10 @@
                     combine(
                         keyguardTransitionInteractor.finishedKeyguardState,
                         keyguardInteractor.statusBarState,
-                    ) { keyguardState, statusBarState ->
-                        Pair(keyguardState, statusBarState)
+                    ) { a, b ->
+                        Pair(a, b)
                     },
-                    { shadeModel, pair -> Triple(shadeModel, pair.first, pair.second) }
+                    { a, bc -> Triple(a, bc.first, bc.second) }
                 )
                 .collect { triple ->
                     val (shadeModel, keyguardState, statusBarState) = triple
@@ -116,8 +126,7 @@
                         )
                     } else {
                         // TODO (b/251849525): Remove statusbarstate check when that state is
-                        // integrated
-                        // into KeyguardTransitionRepository
+                        // integrated into KeyguardTransitionRepository
                         if (
                             keyguardState == KeyguardState.LOCKSCREEN &&
                                 shadeModel.isUserDragging &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
index 6c1adbd..4100f7a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collect
@@ -34,23 +35,27 @@
 constructor(
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val keyguardTransitionRepository: KeyguardTransitionRepository,
 ) : TransitionInteractor("LOCKSCREEN->GONE") {
 
     override fun start() {
         scope.launch {
-            keyguardInteractor.isKeyguardShowing.collect { isShowing ->
-                if (!isShowing) {
-                    keyguardTransitionRepository.startTransition(
-                        TransitionInfo(
-                            name,
-                            KeyguardState.LOCKSCREEN,
-                            KeyguardState.GONE,
-                            getAnimator(),
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isKeyguardGoingAway, keyguardState) = pair
+                    if (!isKeyguardGoingAway && keyguardState == KeyguardState.LOCKSCREEN) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.LOCKSCREEN,
+                                KeyguardState.GONE,
+                                getAnimator(),
+                            )
                         )
-                    )
+                    }
                 }
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index c22f4e7a..fcd653b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -210,9 +210,12 @@
             expansion == KeyguardBouncer.EXPANSION_HIDDEN &&
                 oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN
         ) {
-            repository.setPrimaryVisible(false)
-            repository.setPrimaryShow(null)
-            falsingCollector.onBouncerHidden()
+            /*
+             * There are cases where #hide() was not invoked, such as when
+             * NotificationPanelViewController controls the hide animation. Make sure the state gets
+             * updated by calling #hide() directly.
+             */
+            hide()
             DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
             primaryBouncerCallbackInteractor.dispatchFullyHidden()
         } else if (
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 37f33af..dbffeab 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -46,5 +46,19 @@
 
     @Binds
     @IntoSet
+    abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor
+
+    @Binds
+    @IntoSet
     abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun dreamingLockscreen(
+        impl: DreamingLockscreenTransitionInteractor
+    ): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun dreamingToAod(impl: DreamingToAodTransitionInteractor): TransitionInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index 7958033..dd908c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -17,12 +17,29 @@
 
 /** List of all possible states to transition to/from */
 enum class KeyguardState {
-    /**
-     * For initialization as well as when the security method is set to NONE, indicating that
-     * the keyguard should never be shown.
+    /*
+     * The display is completely off, as well as any sensors that would trigger the device to wake
+     * up.
      */
-    NONE,
-    /* Always-on Display. The device is in a low-power mode with a minimal UI visible */
+    OFF,
+    /**
+     * The device has entered a special low-power mode within SystemUI. Doze is technically a
+     * special dream service implementation. No UI is visible. In this state, a least some
+     * low-powered sensors such as lift to wake or tap to wake are enabled, or wake screen for
+     * notifications is enabled, allowing the device to quickly wake up.
+     */
+    DOZING,
+    /*
+     * A device state after the device times out, which can be from both LOCKSCREEN or GONE states.
+     * DOZING is an example of special version of this state. Dreams may be implemented by third
+     * parties to present their own UI over keyguard, like a screensaver.
+     */
+    DREAMING,
+    /**
+     * The device has entered a special low-power mode within SystemUI, also called the Always-on
+     * Display (AOD). A minimal UI is presented to show critical information. If the device is in
+     * low-power mode without a UI, then it is DOZING.
+     */
     AOD,
     /*
      * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS
@@ -34,7 +51,6 @@
      * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
      */
     LOCKSCREEN,
-
     /*
      * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard
      * is being removed, but there are other cases where the user is swiping away keyguard, such as
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
index 0e0465b..38a93b5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
@@ -17,7 +17,12 @@
 
 /** Possible states for a running transition between [State] */
 enum class TransitionState {
+    /* Transition has begun. */
     STARTED,
+    /* Transition is actively running. */
     RUNNING,
-    FINISHED
+    /* Transition has completed successfully. */
+    FINISHED,
+    /* Transition has been interrupted, and not completed successfully. */
+    CANCELED,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
index 732a6f7..767fd58 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -17,8 +17,8 @@
 
 /** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */
 data class TransitionStep(
-    val from: KeyguardState = KeyguardState.NONE,
-    val to: KeyguardState = KeyguardState.NONE,
+    val from: KeyguardState = KeyguardState.OFF,
+    val to: KeyguardState = KeyguardState.OFF,
     val value: Float = 0f, // constrained [0.0, 1.0]
     val transitionState: TransitionState = TransitionState.FINISHED,
     val ownerName: String = "",
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
index 64f834d..92040f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
@@ -24,5 +24,15 @@
     /** Device is now fully awake and interactive. */
     AWAKE,
     /** Signal that the device is now going to sleep. */
-    STARTING_TO_SLEEP,
+    STARTING_TO_SLEEP;
+
+    companion object {
+        fun isSleepingOrStartingToSleep(model: WakefulnessModel): Boolean {
+            return model == ASLEEP || model == STARTING_TO_SLEEP
+        }
+
+        fun isWakingOrStartingToWake(model: WakefulnessModel): Boolean {
+            return model == AWAKE || model == STARTING_TO_WAKE
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index a22958b..7739a45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -94,6 +94,10 @@
                     viewModel.setBouncerViewDelegate(delegate)
                     launch {
                         viewModel.show.collect {
+                            hostViewController.showPromptReason(it.promptReason)
+                            it.errorMessage?.let { errorMessage ->
+                                hostViewController.showErrorMessage(errorMessage)
+                            }
                             hostViewController.showPrimarySecurityScreen()
                             hostViewController.appear(
                                 SystemBarUtils.getStatusBarHeight(view.context)
@@ -102,18 +106,6 @@
                     }
 
                     launch {
-                        viewModel.showPromptReason.collect { prompt ->
-                            hostViewController.showPromptReason(prompt)
-                        }
-                    }
-
-                    launch {
-                        viewModel.showBouncerErrorMessage.collect { errorMessage ->
-                            hostViewController.showErrorMessage(errorMessage)
-                        }
-                    }
-
-                    launch {
                         viewModel.showWithFullExpansion.collect { model ->
                             hostViewController.resetSecurityContainer()
                             hostViewController.showPromptReason(model.promptReason)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 0781600..526ae74 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -26,7 +26,6 @@
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 
 /** Models UI state for the lock screen bouncer; handles user input. */
@@ -45,13 +44,6 @@
     /** Observe whether bouncer is showing. */
     val show: Flow<KeyguardBouncerModel> = interactor.show
 
-    /** Observe bouncer prompt when bouncer is showing. */
-    val showPromptReason: Flow<Int> = interactor.show.map { it.promptReason }
-
-    /** Observe bouncer error message when bouncer is showing. */
-    val showBouncerErrorMessage: Flow<CharSequence> =
-        interactor.show.map { it.errorMessage }.filterNotNull()
-
     /** Observe visible expansion when bouncer is showing. */
     val showWithFullExpansion: Flow<KeyguardBouncerModel> =
         interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
index b682bd1..d4991f9 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
@@ -148,6 +148,7 @@
 
         val currentRotation: Int = display.rotation
         val displayWidthPx = windowMetrics.bounds.width()
+        val displayHeightPx = windowMetrics.bounds.height()
         val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
         val isTablet = isTablet(context)
         val taskbarSize =
@@ -163,6 +164,7 @@
             measuredWidth,
             measuredHeight,
             displayWidthPx,
+            displayHeightPx,
             taskbarSize,
             isTablet,
             currentRotation,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index cffd28f..19bb15a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -24,8 +24,9 @@
 import android.os.Looper
 import android.os.ResultReceiver
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.widget.AdapterView
-import android.widget.AdapterView.OnItemClickListener
 import android.widget.ArrayAdapter
 import android.widget.Spinner
 import android.widget.Switch
@@ -47,6 +48,7 @@
     private val onStartRecordingClicked: Runnable?
 ) : BaseScreenSharePermissionDialog(context, createOptionList(), null) {
     private lateinit var tapsSwitch: Switch
+    private lateinit var tapsView: View
     private lateinit var audioSwitch: Switch
     private lateinit var options: Spinner
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -84,16 +86,25 @@
     private fun initRecordOptionsView() {
         audioSwitch = findViewById(R.id.screenrecord_audio_switch)
         tapsSwitch = findViewById(R.id.screenrecord_taps_switch)
+        tapsView = findViewById(R.id.show_taps)
+        updateTapsViewVisibility()
         options = findViewById(R.id.screen_recording_options)
         val a: ArrayAdapter<*> =
             ScreenRecordingAdapter(context, android.R.layout.simple_spinner_dropdown_item, MODES)
         a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
         options.adapter = a
-        options.setOnItemClickListenerInt(
-            OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
-                audioSwitch.isChecked = true
-            }
-        )
+        options.setOnItemClickListenerInt { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
+            audioSwitch.isChecked = true
+        }
+    }
+
+    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
+        super.onItemSelected(adapterView, view, pos, id)
+        updateTapsViewVisibility()
+    }
+
+    private fun updateTapsViewVisibility() {
+        tapsView.visibility = if (selectedScreenShareOption.mode == SINGLE_APP) GONE else VISIBLE
     }
 
     /**
@@ -103,7 +114,7 @@
      */
     private fun requestScreenCapture(captureTarget: MediaProjectionCaptureTarget?) {
         val userContext = userContextProvider.userContext
-        val showTaps = tapsSwitch.isChecked
+        val showTaps = selectedScreenShareOption.mode != SINGLE_APP && tapsSwitch.isChecked
         val audioMode =
             if (audioSwitch.isChecked) options.selectedItem as ScreenRecordingAudioSource
             else ScreenRecordingAudioSource.NONE
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index c41e2bc..4cb91e1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -15,12 +15,17 @@
  */
 package com.android.systemui.screenshot
 
-import android.app.Service
 import android.content.Intent
 import android.os.IBinder
 import android.util.Log
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.phone.CentralSurfaces
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import java.util.Optional
 import javax.inject.Inject
 
@@ -30,7 +35,8 @@
 internal class ScreenshotProxyService @Inject constructor(
     private val mExpansionMgr: ShadeExpansionStateManager,
     private val mCentralSurfacesOptional: Optional<CentralSurfaces>,
-) : Service() {
+    @Main private val mMainDispatcher: CoroutineDispatcher,
+) : LifecycleService() {
 
     private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
         /**
@@ -43,20 +49,28 @@
         }
 
         override fun dismissKeyguard(callback: IOnDoneCallback) {
-            if (mCentralSurfacesOptional.isPresent) {
-                mCentralSurfacesOptional.get().executeRunnableDismissingKeyguard(
-                    Runnable {
-                        callback.onDone(true)
-                    }, null,
-                    true /* dismissShade */, true /* afterKeyguardGone */,
-                    true /* deferred */
-                )
-            } else {
-                callback.onDone(false)
+            lifecycleScope.launch {
+                executeAfterDismissing(callback)
             }
         }
     }
 
+    private suspend fun executeAfterDismissing(callback: IOnDoneCallback) =
+        withContext(mMainDispatcher) {
+            mCentralSurfacesOptional.ifPresentOrElse(
+                    {
+                        it.executeRunnableDismissingKeyguard(
+                                Runnable {
+                                    callback.onDone(true)
+                                }, null,
+                                true /* dismissShade */, true /* afterKeyguardGone */,
+                                true /* deferred */
+                        )
+                    },
+                    { callback.onDone(false) }
+            )
+        }
+
     override fun onBind(intent: Intent): IBinder? {
         Log.d(TAG, "onBind: $intent")
         return mBinder
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
new file mode 100644
index 0000000..fc61e90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
@@ -0,0 +1,73 @@
+/*
+ * 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.shade;
+
+import com.android.systemui.camera.CameraGestureHelper;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
+
+import javax.inject.Inject;
+
+/** Handles launching camera from Shade. */
+@SysUISingleton
+public class CameraLauncher {
+    private final CameraGestureHelper mCameraGestureHelper;
+    private final KeyguardBypassController mKeyguardBypassController;
+
+    private boolean mLaunchingAffordance;
+
+    @Inject
+    public CameraLauncher(
+            CameraGestureHelper cameraGestureHelper,
+            KeyguardBypassController keyguardBypassController
+    ) {
+        mCameraGestureHelper = cameraGestureHelper;
+        mKeyguardBypassController = keyguardBypassController;
+    }
+
+    /** Launches the camera. */
+    public void launchCamera(int source, boolean isShadeFullyCollapsed) {
+        if (!isShadeFullyCollapsed) {
+            setLaunchingAffordance(true);
+        }
+
+        mCameraGestureHelper.launchCamera(source);
+    }
+
+    /**
+     * Set whether we are currently launching an affordance. This is currently only set when
+     * launched via a camera gesture.
+     */
+    public void setLaunchingAffordance(boolean launchingAffordance) {
+        mLaunchingAffordance = launchingAffordance;
+        mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
+    }
+
+    /**
+     * Return true when a bottom affordance is launching an occluded activity with a splash screen.
+     */
+    public boolean isLaunchingAffordance() {
+        return mLaunchingAffordance;
+    }
+
+    /**
+     * Whether the camera application can be launched for the camera launch gesture.
+     */
+    public boolean canCameraGestureBeLaunched(int barState) {
+        return mCameraGestureHelper.canCameraGestureBeLaunched(barState);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 8c1b574..ceef8c8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -124,7 +124,6 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.qualifiers.DisplayId;
@@ -464,7 +463,6 @@
     private boolean mCollapsedOnDown;
     private boolean mClosingWithAlphaFadeOut;
     private boolean mHeadsUpAnimatingAway;
-    private boolean mLaunchingAffordance;
     private final FalsingManager mFalsingManager;
     private final FalsingCollector mFalsingCollector;
 
@@ -575,7 +573,7 @@
 
     /** Whether the current animator is resetting the pulse expansion after a drag down. */
     private boolean mIsPulseExpansionResetAnimator;
-    private final Rect mKeyguardStatusAreaClipBounds = new Rect();
+    private final Rect mLastQsClipBounds = new Rect();
     private final Region mQsInterceptRegion = new Region();
     /** Alpha of the views which only show on the keyguard but not in shade / shade locked. */
     private float mKeyguardOnlyContentAlpha = 1.0f;
@@ -615,7 +613,6 @@
     private final NotificationListContainer mNotificationListContainer;
     private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     private final NPVCDownEventState.Buffer mLastDownEvents;
-    private final CameraGestureHelper mCameraGestureHelper;
     private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     private float mMinExpandHeight;
@@ -743,7 +740,6 @@
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             ShadeTransitionController shadeTransitionController,
             SystemClock systemClock,
-            CameraGestureHelper cameraGestureHelper,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
             DumpManager dumpManager) {
@@ -924,7 +920,6 @@
                         unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay);
                     }
                 });
-        mCameraGestureHelper = cameraGestureHelper;
         mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
         dumpManager.registerDumpable(this);
     }
@@ -2814,7 +2809,7 @@
      */
     private void applyQSClippingBounds(int left, int top, int right, int bottom,
             boolean qsVisible) {
-        if (!mAnimateNextNotificationBounds || mKeyguardStatusAreaClipBounds.isEmpty()) {
+        if (!mAnimateNextNotificationBounds || mLastQsClipBounds.isEmpty()) {
             if (mQsClippingAnimation != null) {
                 // update the end position of the animator
                 mQsClippingAnimationEndBounds.set(left, top, right, bottom);
@@ -2823,10 +2818,10 @@
             }
         } else {
             mQsClippingAnimationEndBounds.set(left, top, right, bottom);
-            final int startLeft = mKeyguardStatusAreaClipBounds.left;
-            final int startTop = mKeyguardStatusAreaClipBounds.top;
-            final int startRight = mKeyguardStatusAreaClipBounds.right;
-            final int startBottom = mKeyguardStatusAreaClipBounds.bottom;
+            final int startLeft = mLastQsClipBounds.left;
+            final int startTop = mLastQsClipBounds.top;
+            final int startRight = mLastQsClipBounds.right;
+            final int startBottom = mLastQsClipBounds.bottom;
             if (mQsClippingAnimation != null) {
                 mQsClippingAnimation.cancel();
             }
@@ -2863,12 +2858,10 @@
 
     private void applyQSClippingImmediately(int left, int top, int right, int bottom,
             boolean qsVisible) {
-        // Fancy clipping for quick settings
         int radius = mScrimCornerRadius;
         boolean clipStatusView = false;
+        mLastQsClipBounds.set(left, top, right, bottom);
         if (mIsFullWidth) {
-            // The padding on this area is large enough that we can use a cheaper clipping strategy
-            mKeyguardStatusAreaClipBounds.set(left, top, right, bottom);
             clipStatusView = qsVisible;
             float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius;
             radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius,
@@ -2903,8 +2896,8 @@
                     radius,
                     qsVisible && !mSplitShadeEnabled);
         }
-        mKeyguardStatusViewController.setClipBounds(
-                clipStatusView ? mKeyguardStatusAreaClipBounds : null);
+        // The padding on this area is large enough that we can use a cheaper clipping strategy
+        mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
         if (!qsVisible && mSplitShadeEnabled) {
             // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
             // be visible, otherwise you can see the bounds once swiping up to see bouncer
@@ -3948,6 +3941,10 @@
         }
     }
 
+    public int getBarState() {
+        return mBarState;
+    }
+
     private boolean isOnKeyguard() {
         return mBarState == KEYGUARD;
     }
@@ -3993,35 +3990,6 @@
                 && mBarState == StatusBarState.SHADE;
     }
 
-    /** Launches the camera. */
-    public void launchCamera(int source) {
-        if (!isFullyCollapsed()) {
-            setLaunchingAffordance(true);
-        }
-
-        mCameraGestureHelper.launchCamera(source);
-    }
-
-    public void onAffordanceLaunchEnded() {
-        setLaunchingAffordance(false);
-    }
-
-    /** Set whether we are currently launching an affordance (i.e. camera gesture). */
-    private void setLaunchingAffordance(boolean launchingAffordance) {
-        mLaunchingAffordance = launchingAffordance;
-        mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
-    }
-
-    /** Returns whether a bottom affordance is launching an occluded activity with splash screen. */
-    public boolean isLaunchingAffordanceWithPreview() {
-        return mLaunchingAffordance;
-    }
-
-    /** Whether the camera application can be launched by the camera launch gesture. */
-    public boolean canCameraGestureBeLaunched() {
-        return mCameraGestureHelper.canCameraGestureBeLaunched(mBarState);
-    }
-
     public boolean hideStatusBarIconsWhenExpanded() {
         if (mIsLaunchAnimationRunning) {
             return mHideIconsDuringLaunchAnimation;
@@ -4360,7 +4328,6 @@
         ipw.print("mCollapsedOnDown="); ipw.println(mCollapsedOnDown);
         ipw.print("mClosingWithAlphaFadeOut="); ipw.println(mClosingWithAlphaFadeOut);
         ipw.print("mHeadsUpAnimatingAway="); ipw.println(mHeadsUpAnimatingAway);
-        ipw.print("mLaunchingAffordance="); ipw.println(mLaunchingAffordance);
         ipw.print("mShowIconsWhenExpanded="); ipw.println(mShowIconsWhenExpanded);
         ipw.print("mIndicationBottomPadding="); ipw.println(mIndicationBottomPadding);
         ipw.print("mAmbientIndicationBottomPadding="); ipw.println(mAmbientIndicationBottomPadding);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 3670d09..0deb47d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1263,7 +1263,8 @@
             showErrorMessageNowOrLater(errString, followupMessage);
         } else if (!mAuthController.isUdfpsFingerDown()) {
             // On subsequent lockouts, we show a more generic locked out message.
-            showBiometricMessage(mContext.getString(R.string.keyguard_face_unlock_unavailable),
+            showErrorMessageNowOrLater(
+                    mContext.getString(R.string.keyguard_face_unlock_unavailable),
                     followupMessage);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
index ec221b7..c523d22 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
@@ -15,9 +15,7 @@
  */
 package com.android.systemui.statusbar.connectivity;
 
-import static com.android.settingslib.mobile.MobileMappings.getDefaultIcons;
-import static com.android.settingslib.mobile.MobileMappings.getIconKey;
-import static com.android.settingslib.mobile.MobileMappings.mapIconSets;
+import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID;
 
 import android.content.Context;
 import android.content.Intent;
@@ -46,6 +44,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.SignalStrengthUtil;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import java.io.PrintWriter;
@@ -63,6 +62,7 @@
     private final TelephonyManager mPhone;
     private final CarrierConfigTracker mCarrierConfigTracker;
     private final SubscriptionDefaults mDefaults;
+    private final MobileMappingsProxy mMobileMappingsProxy;
     private final String mNetworkNameDefault;
     private final String mNetworkNameSeparator;
     private final ContentObserver mObserver;
@@ -121,6 +121,7 @@
             TelephonyManager phone,
             CallbackHandler callbackHandler,
             NetworkControllerImpl networkController,
+            MobileMappingsProxy mobileMappingsProxy,
             SubscriptionInfo info,
             SubscriptionDefaults defaults,
             Looper receiverLooper,
@@ -135,13 +136,14 @@
         mPhone = phone;
         mDefaults = defaults;
         mSubscriptionInfo = info;
+        mMobileMappingsProxy = mobileMappingsProxy;
         mNetworkNameSeparator = getTextIfExists(
                 R.string.status_bar_network_name_separator).toString();
         mNetworkNameDefault = getTextIfExists(
                 com.android.internal.R.string.lockscreen_carrier_default).toString();
 
-        mNetworkToIconLookup = mapIconSets(mConfig);
-        mDefaultIcons = getDefaultIcons(mConfig);
+        mNetworkToIconLookup = mMobileMappingsProxy.mapIconSets(mConfig);
+        mDefaultIcons = mMobileMappingsProxy.getDefaultIcons(mConfig);
 
         String networkName = info.getCarrierName() != null ? info.getCarrierName().toString()
                 : mNetworkNameDefault;
@@ -161,8 +163,8 @@
     void setConfiguration(Config config) {
         mConfig = config;
         updateInflateSignalStrength();
-        mNetworkToIconLookup = mapIconSets(mConfig);
-        mDefaultIcons = getDefaultIcons(mConfig);
+        mNetworkToIconLookup = mMobileMappingsProxy.mapIconSets(mConfig);
+        mDefaultIcons = mMobileMappingsProxy.getDefaultIcons(mConfig);
         updateTelephony();
     }
 
@@ -271,8 +273,9 @@
             dataContentDescription = mContext.getString(R.string.data_connection_no_internet);
         }
 
-        final QsInfo qsInfo = getQsInfo(contentDescription, icons.dataType);
-        final SbInfo sbInfo = getSbInfo(contentDescription, icons.dataType);
+        int iconId = mCurrentState.getNetworkTypeIcon(mContext);
+        final QsInfo qsInfo = getQsInfo(contentDescription, iconId);
+        final SbInfo sbInfo = getSbInfo(contentDescription, iconId);
 
         MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
                 sbInfo.icon,
@@ -373,6 +376,10 @@
         } else if (action.equals(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) {
             updateDataSim();
             notifyListenersIfNecessary();
+        } else if (action.equals(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED)) {
+            int carrierId = intent.getIntExtra(
+                    TelephonyManager.EXTRA_CARRIER_ID, UNKNOWN_CARRIER_ID);
+            mCurrentState.setCarrierId(carrierId);
         }
     }
 
@@ -477,7 +484,8 @@
             mCurrentState.level = getSignalLevel(mCurrentState.signalStrength);
         }
 
-        String iconKey = getIconKey(mCurrentState.telephonyDisplayInfo);
+        mCurrentState.setCarrierId(mPhone.getSimCarrierId());
+        String iconKey = mMobileMappingsProxy.getIconKey(mCurrentState.telephonyDisplayInfo);
         if (mNetworkToIconLookup.get(iconKey) != null) {
             mCurrentState.iconGroup = mNetworkToIconLookup.get(iconKey);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt
index 7938179..a323454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.MobileStatusTracker
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import javax.inject.Inject
 
@@ -33,6 +34,7 @@
     val context: Context,
     val callbackHandler: CallbackHandler,
     val carrierConfigTracker: CarrierConfigTracker,
+    val mobileMappings: MobileMappingsProxy,
 ) {
     fun createMobileSignalController(
         config: MobileMappings.Config,
@@ -56,6 +58,7 @@
             phone,
             callbackHandler,
             networkController,
+            mobileMappings,
             subscriptionInfo,
             subscriptionDefaults,
             receiverLooper,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
index f20d206..1fb6a98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
@@ -16,10 +16,14 @@
 
 package com.android.systemui.statusbar.connectivity
 
+import android.annotation.DrawableRes
+import android.content.Context
 import android.telephony.ServiceState
 import android.telephony.SignalStrength
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.Utils
 import com.android.settingslib.mobile.MobileStatusTracker.MobileStatus
 import com.android.settingslib.mobile.TelephonyIcons
@@ -41,7 +45,7 @@
     @JvmField var roaming: Boolean = false,
     @JvmField var dataState: Int = TelephonyManager.DATA_DISCONNECTED,
     // Tracks the on/off state of the defaultDataSubscription
-    @JvmField var defaultDataOff: Boolean = false
+    @JvmField var defaultDataOff: Boolean = false,
 ) : ConnectivityState() {
 
     @JvmField var telephonyDisplayInfo = TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN,
@@ -49,6 +53,11 @@
     @JvmField var serviceState: ServiceState? = null
     @JvmField var signalStrength: SignalStrength? = null
 
+    var carrierId = TelephonyManager.UNKNOWN_CARRIER_ID
+
+    @VisibleForTesting
+    var networkTypeResIdCache: NetworkTypeResIdCache = NetworkTypeResIdCache()
+
     /** @return true if this state is disabled or not default data */
     val isDataDisabledOrNotDefault: Boolean
         get() = (iconGroup === TelephonyIcons.DATA_DISABLED ||
@@ -125,6 +134,21 @@
         return serviceState != null && serviceState!!.roaming
     }
 
+    /**
+     *
+     * Load the (potentially customized) icon resource id for the current network type. Note that
+     * this operation caches the result. Note that reading the [MobileIconGroup.dataType] field
+     * directly will not yield correct results in cases where the carrierId has an associated
+     * override. This is the preferred method for getting the network type indicator.
+     *
+     * @return a drawable res id appropriate for the current (carrierId, networkType) pair
+     */
+    @DrawableRes
+    fun getNetworkTypeIcon(context: Context): Int {
+        val icon = (iconGroup as MobileIconGroup)
+        return networkTypeResIdCache.get(icon, carrierId, context)
+    }
+
     fun setFromMobileStatus(mobileStatus: MobileStatus) {
         activityIn = mobileStatus.activityIn
         activityOut = mobileStatus.activityOut
@@ -140,6 +164,7 @@
         super.toString(builder)
         builder.append(',')
         builder.append("dataSim=$dataSim,")
+        builder.append("carrierId=$carrierId")
         builder.append("networkName=$networkName,")
         builder.append("networkNameData=$networkNameData,")
         builder.append("dataConnected=$dataConnected,")
@@ -157,6 +182,8 @@
         builder.append("voiceServiceState=${getVoiceServiceState()},")
         builder.append("isInService=${isInService()},")
 
+        builder.append("networkTypeIconCache=$networkTypeResIdCache")
+
         builder.append("serviceState=${serviceState?.minLog() ?: "(null)"},")
         builder.append("signalStrength=${signalStrength?.minLog() ?: "(null)"},")
         builder.append("displayInfo=$telephonyDisplayInfo")
@@ -164,6 +191,7 @@
 
     override fun tableColumns(): List<String> {
         val columns = listOf("dataSim",
+            "carrierId",
             "networkName",
             "networkNameData",
             "dataConnected",
@@ -178,6 +206,7 @@
             "showQuickSettingsRatIcon",
             "voiceServiceState",
             "isInService",
+            "networkTypeIconCache",
             "serviceState",
             "signalStrength",
             "displayInfo")
@@ -187,6 +216,7 @@
 
     override fun tableData(): List<String> {
         val columns = listOf(dataSim,
+                carrierId,
                 networkName,
                 networkNameData,
                 dataConnected,
@@ -201,6 +231,7 @@
                 showQuickSettingsRatIcon(),
                 getVoiceServiceState(),
                 isInService(),
+                networkTypeResIdCache,
                 serviceState?.minLog() ?: "(null)",
                 signalStrength?.minLog() ?: "(null)",
                 telephonyDisplayInfo).map { it.toString() }
@@ -217,6 +248,7 @@
 
         if (networkName != other.networkName) return false
         if (networkNameData != other.networkNameData) return false
+        if (carrierId != other.carrierId) return false
         if (dataSim != other.dataSim) return false
         if (dataConnected != other.dataConnected) return false
         if (isEmergency != other.isEmergency) return false
@@ -238,6 +270,7 @@
         var result = super.hashCode()
         result = 31 * result + (networkName?.hashCode() ?: 0)
         result = 31 * result + (networkNameData?.hashCode() ?: 0)
+        result = 31 * result + (carrierId.hashCode())
         result = 31 * result + dataSim.hashCode()
         result = 31 * result + dataConnected.hashCode()
         result = 31 * result + isEmergency.hashCode()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 450b757..73d6483 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -22,6 +22,7 @@
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
@@ -138,7 +139,7 @@
     private final MobileSignalControllerFactory mMobileFactory;
 
     private TelephonyCallback.ActiveDataSubscriptionIdListener mPhoneStateListener;
-    private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    private int mActiveMobileDataSubscription = INVALID_SUBSCRIPTION_ID;
 
     // Subcontrollers.
     @VisibleForTesting
@@ -502,6 +503,7 @@
         filter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
         filter.addAction(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED);
         filter.addAction(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED);
+        filter.addAction(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
         filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
         mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler);
         mListening = true;
@@ -792,6 +794,20 @@
                 mConfig = Config.readConfig(mContext);
                 mReceiverHandler.post(this::handleConfigurationChanged);
                 break;
+
+            case TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED: {
+                // Notify the relevant MobileSignalController of the change
+                int subId = intent.getIntExtra(
+                        TelephonyManager.EXTRA_SUBSCRIPTION_ID,
+                        INVALID_SUBSCRIPTION_ID
+                );
+                if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                    if (mMobileSignalControllers.indexOfKey(subId) >= 0) {
+                        mMobileSignalControllers.get(subId).handleBroadcast(intent);
+                    }
+                }
+            }
+            break;
             case Intent.ACTION_SIM_STATE_CHANGED:
                 // Avoid rebroadcast because SysUI is direct boot aware.
                 if (intent.getBooleanExtra(Intent.EXTRA_REBROADCAST_ON_UNLOCK, false)) {
@@ -819,7 +835,7 @@
                 break;
             default:
                 int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
-                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+                        INVALID_SUBSCRIPTION_ID);
                 if (SubscriptionManager.isValidSubscriptionId(subId)) {
                     if (mMobileSignalControllers.indexOfKey(subId) >= 0) {
                         mMobileSignalControllers.get(subId).handleBroadcast(intent);
@@ -1335,6 +1351,9 @@
             String slotString = args.getString("slot");
             int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString);
             slot = MathUtils.constrain(slot, 0, 8);
+            String carrierIdString = args.getString("carrierid");
+            int carrierId = TextUtils.isEmpty(carrierIdString) ? 0
+                    : Integer.parseInt(carrierIdString);
             // Ensure we have enough sim slots
             List<SubscriptionInfo> subs = new ArrayList<>();
             while (mMobileSignalControllers.size() <= slot) {
@@ -1346,6 +1365,9 @@
             }
             // Hack to index linearly for easy use.
             MobileSignalController controller = mMobileSignalControllers.valueAt(slot);
+            if (carrierId != 0) {
+                controller.getState().setCarrierId(carrierId);
+            }
             controller.getState().dataSim = datatype != null;
             controller.getState().isDefault = datatype != null;
             controller.getState().dataConnected = datatype != null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt
new file mode 100644
index 0000000..9be7ee9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.connectivity
+
+import android.annotation.DrawableRes
+import android.content.Context
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
+import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
+
+/**
+ * Cache for network type resource IDs.
+ *
+ * The default framework behavior is to have a statically defined icon per network type. See
+ * [MobileIconGroup] for the standard mapping.
+ *
+ * For the case of carrierId-defined overrides, we want to check [MobileIconCarrierIdOverrides] for
+ * an existing icon override, and cache the result of the operation
+ */
+class NetworkTypeResIdCache(
+    private val overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
+) {
+    @DrawableRes private var cachedResId: Int = 0
+    private var lastCarrierId: Int? = null
+    private var lastIconGroup: MobileIconGroup? = null
+    private var isOverridden: Boolean = false
+
+    @DrawableRes
+    fun get(iconGroup: MobileIconGroup, carrierId: Int, context: Context): Int {
+        if (lastCarrierId != carrierId || lastIconGroup != iconGroup) {
+            lastCarrierId = carrierId
+            lastIconGroup = iconGroup
+
+            val maybeOverride = calculateOverriddenIcon(iconGroup, carrierId, context)
+            if (maybeOverride > 0) {
+                cachedResId = maybeOverride
+                isOverridden = true
+            } else {
+                cachedResId = iconGroup.dataType
+                isOverridden = false
+            }
+        }
+
+        return cachedResId
+    }
+
+    override fun toString(): String {
+        return "networkTypeResIdCache={id=$cachedResId, isOverridden=$isOverridden}"
+    }
+
+    @DrawableRes
+    private fun calculateOverriddenIcon(
+        iconGroup: MobileIconGroup,
+        carrierId: Int,
+        context: Context,
+    ): Int {
+        val name = iconGroup.name
+        if (!overrides.carrierIdEntryExists(carrierId)) {
+            return 0
+        }
+
+        return overrides.getOverrideFor(carrierId, name, context.resources)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt
index a02dd34..42b874f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt
@@ -37,6 +37,13 @@
  * own [Configuration] and track resources based on the full set of available mcc-mnc combinations.
  *
  * (for future reference: b/240555502 is the initiating bug for this)
+ *
+ * NOTE: MCC/MNC qualifiers are not sufficient to fully describe a network type icon qualified by
+ * network type + carrier ID. This class exists to keep the legacy behavior of using the MCC/MNC
+ * resource qualifiers working, but if a carrier-specific icon is requested, then the override
+ * provided by [MobileIconCarrierIdOverrides] will take precedence.
+ *
+ * TODO(b/258503704): consider removing this class in favor of the `carrierId` overrides
  */
 @SysUISingleton
 class MobileContextProvider
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 41f0520..f3482f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -55,6 +55,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -71,6 +72,8 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /** */
 @CentralSurfacesComponent.CentralSurfacesScope
 public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callbacks {
@@ -99,6 +102,7 @@
     private final boolean mVibrateOnOpening;
     private final VibrationEffect mCameraLaunchGestureVibrationEffect;
     private final SystemBarAttributesListener mSystemBarAttributesListener;
+    private final Lazy<CameraLauncher> mCameraLauncherLazy;
 
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
@@ -128,8 +132,8 @@
             Optional<Vibrator> vibratorOptional,
             DisableFlagsLogger disableFlagsLogger,
             @DisplayId int displayId,
-            SystemBarAttributesListener systemBarAttributesListener) {
-
+            SystemBarAttributesListener systemBarAttributesListener,
+            Lazy<CameraLauncher> cameraLauncherLazy) {
         mCentralSurfaces = centralSurfaces;
         mContext = context;
         mShadeController = shadeController;
@@ -152,6 +156,7 @@
         mVibratorOptional = vibratorOptional;
         mDisableFlagsLogger = disableFlagsLogger;
         mDisplayId = displayId;
+        mCameraLauncherLazy = cameraLauncherLazy;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -346,7 +351,8 @@
             mCentralSurfaces.setLaunchCameraOnFinishedGoingToSleep(true);
             return;
         }
-        if (!mNotificationPanelViewController.canCameraGestureBeLaunched()) {
+        if (!mCameraLauncherLazy.get().canCameraGestureBeLaunched(
+                mNotificationPanelViewController.getBarState())) {
             if (CentralSurfaces.DEBUG_CAMERA_LIFT) {
                 Slog.d(CentralSurfaces.TAG, "Can't launch camera right now");
             }
@@ -383,7 +389,8 @@
                 if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                     mStatusBarKeyguardViewManager.reset(true /* hide */);
                 }
-                mNotificationPanelViewController.launchCamera(source);
+                mCameraLauncherLazy.get().launchCamera(source,
+                        mNotificationPanelViewController.isFullyCollapsed());
                 mCentralSurfaces.updateScrimController();
             } else {
                 // We need to defer the camera launch until the screen comes on, since otherwise
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 71609f8..334f1af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -175,6 +175,7 @@
 import com.android.systemui.ripple.RippleShader.RippleShape;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -468,6 +469,7 @@
     private final PluginManager mPluginManager;
     private final ShadeController mShadeController;
     private final InitController mInitController;
+    private final Lazy<CameraLauncher> mCameraLauncherLazy;
 
     private final PluginDependencyProvider mPluginDependencyProvider;
     private final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -600,6 +602,7 @@
 
     private Runnable mLaunchTransitionEndRunnable;
     private Runnable mLaunchTransitionCancelRunnable;
+    private boolean mLaunchingAffordance;
     private boolean mLaunchCameraWhenFinishedWaking;
     private boolean mLaunchCameraOnFinishedGoingToSleep;
     private boolean mLaunchEmergencyActionWhenFinishedWaking;
@@ -744,7 +747,8 @@
             InteractionJankMonitor jankMonitor,
             DeviceStateManager deviceStateManager,
             WiredChargingRippleController wiredChargingRippleController,
-            IDreamManager dreamManager) {
+            IDreamManager dreamManager,
+            Lazy<CameraLauncher> cameraLauncherLazy) {
         mContext = context;
         mNotificationsController = notificationsController;
         mFragmentService = fragmentService;
@@ -821,6 +825,7 @@
         mMessageRouter = messageRouter;
         mWallpaperManager = wallpaperManager;
         mJankMonitor = jankMonitor;
+        mCameraLauncherLazy = cameraLauncherLazy;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
@@ -2949,7 +2954,7 @@
 
     private void onLaunchTransitionFadingEnded() {
         mNotificationPanelViewController.resetAlpha();
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
         releaseGestureWakeLock();
         runLaunchTransitionEndRunnable();
         mKeyguardStateController.setLaunchTransitionFadingAway(false);
@@ -3019,7 +3024,7 @@
 
     private void onLaunchTransitionTimeout() {
         Log.w(TAG, "Launch transition: Timeout!");
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
         releaseGestureWakeLock();
         mNotificationPanelViewController.resetViews(false /* animate */);
     }
@@ -3072,7 +3077,7 @@
         }
         mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         releaseGestureWakeLock();
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
         mNotificationPanelViewController.resetAlpha();
         mNotificationPanelViewController.resetTranslation();
         mNotificationPanelViewController.resetViewGroupFade();
@@ -3230,7 +3235,7 @@
     @Override
     public void endAffordanceLaunch() {
         releaseGestureWakeLock();
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
     }
 
     /**
@@ -3503,7 +3508,7 @@
     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
         @Override
         public void onFinishedGoingToSleep() {
-            mNotificationPanelViewController.onAffordanceLaunchEnded();
+            mCameraLauncherLazy.get().setLaunchingAffordance(false);
             releaseGestureWakeLock();
             mLaunchCameraWhenFinishedWaking = false;
             mDeviceInteractive = false;
@@ -3604,7 +3609,8 @@
                         .updateSensitivenessForOccludedWakeup();
             }
             if (mLaunchCameraWhenFinishedWaking) {
-                mNotificationPanelViewController.launchCamera(mLastCameraLaunchSource);
+                mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource,
+                        mNotificationPanelViewController.isFullyCollapsed());
                 mLaunchCameraWhenFinishedWaking = false;
             }
             if (mLaunchEmergencyActionWhenFinishedWaking) {
@@ -3795,8 +3801,7 @@
 
         mScrimController.setExpansionAffectsAlpha(!unlocking);
 
-        boolean launchingAffordanceWithPreview =
-                mNotificationPanelViewController.isLaunchingAffordanceWithPreview();
+        boolean launchingAffordanceWithPreview = mLaunchingAffordance;
         mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview);
 
         if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index d328472..aa0757e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -279,10 +279,7 @@
      * @see #onFullyShown()
      */
     private void onFullyHidden() {
-        cancelShowRunnable();
-        setVisibility(View.INVISIBLE);
-        mFalsingCollector.onBouncerHidden();
-        DejankUtils.postAfterTraversal(mResetRunnable);
+
     }
 
     private void setVisibility(@View.Visibility int visibility) {
@@ -459,7 +456,13 @@
             onFullyShown();
             dispatchFullyShown();
         } else if (fraction == EXPANSION_HIDDEN && oldExpansion != EXPANSION_HIDDEN) {
-            onFullyHidden();
+            DejankUtils.postAfterTraversal(mResetRunnable);
+            /*
+             * There are cases where #hide() was not invoked, such as when
+             * NotificationPanelViewController controls the hide animation. Make sure the state gets
+             * updated by calling #hide() directly.
+             */
+            hide(false /* destroyView */);
             dispatchFullyHidden();
         } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) {
             dispatchStartingToHide();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 86e27ab..d54a863 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -895,7 +895,7 @@
 
         float stateBehind = mClipsQsScrim ? state.getNotifAlpha() : state.getBehindAlpha();
         float behindAlpha;
-        int behindTint;
+        int behindTint = state.getBehindTint();
         if (mDarkenWhileDragging) {
             behindAlpha = MathUtils.lerp(mDefaultScrimAlpha, stateBehind,
                     interpolatedFract);
@@ -903,12 +903,14 @@
             behindAlpha = MathUtils.lerp(0 /* start */, stateBehind,
                     interpolatedFract);
         }
-        if (mClipsQsScrim) {
-            behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(),
+        if (mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()) {
+            if (mClipsQsScrim) {
+                behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(),
                     state.getNotifTint(), interpolatedFract);
-        } else {
-            behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
+            } else {
+                behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
                     state.getBehindTint(), interpolatedFract);
+            }
         }
         if (mQsExpansion > 0) {
             behindAlpha = MathUtils.lerp(behindAlpha, mDefaultScrimAlpha, mQsExpansion);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index e9d4e55..681d21b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -165,6 +165,7 @@
             @Override
             public void onFullyHidden() {
                 mPrimaryBouncerAnimating = false;
+                updateStates();
             }
 
             @Override
@@ -1183,12 +1184,16 @@
             updateNavigationBarVisibility(navBarVisible);
         }
 
-        if (primaryBouncerShowing != mLastPrimaryBouncerShowing || mFirstUpdate) {
+        boolean isPrimaryBouncerShowingChanged =
+            primaryBouncerShowing != mLastPrimaryBouncerShowing;
+        mLastPrimaryBouncerShowing = primaryBouncerShowing;
+
+        if (isPrimaryBouncerShowingChanged || mFirstUpdate) {
             mNotificationShadeWindowController.setBouncerShowing(primaryBouncerShowing);
             mCentralSurfaces.setBouncerShowing(primaryBouncerShowing);
         }
         if (primaryBouncerIsOrWillBeShowing != mLastPrimaryBouncerIsOrWillBeShowing || mFirstUpdate
-                || primaryBouncerShowing != mLastPrimaryBouncerShowing) {
+                || isPrimaryBouncerShowingChanged) {
             mKeyguardUpdateManager.sendPrimaryBouncerChanged(primaryBouncerIsOrWillBeShowing,
                     primaryBouncerShowing);
         }
@@ -1197,7 +1202,6 @@
         mLastShowing = showing;
         mLastGlobalActionsVisible = mGlobalActionsVisible;
         mLastOccluded = occluded;
-        mLastPrimaryBouncerShowing = primaryBouncerShowing;
         mLastPrimaryBouncerIsOrWillBeShowing = primaryBouncerIsOrWillBeShowing;
         mLastBouncerDismissible = primaryBouncerDismissible;
         mLastRemoteInputActive = remoteInputActive;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
index 60bd038..501467f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
@@ -32,6 +32,7 @@
 interface MobileMappingsProxy {
     fun mapIconSets(config: Config): Map<String, MobileIconGroup>
     fun getDefaultIcons(config: Config): MobileIconGroup
+    fun getIconKey(displayInfo: TelephonyDisplayInfo): String
     fun toIconKey(@NetworkType networkType: Int): String
     fun toIconKeyOverride(@NetworkType networkType: Int): String
 }
@@ -44,6 +45,9 @@
     override fun getDefaultIcons(config: Config): MobileIconGroup =
         MobileMappings.getDefaultIcons(config)
 
+    override fun getIconKey(displayInfo: TelephonyDisplayInfo): String =
+        MobileMappings.getIconKey(displayInfo)
+
     override fun toIconKey(@NetworkType networkType: Int): String =
         MobileMappings.toIconKey(networkType)
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index 5d2b0ca..8290084 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -16,8 +16,11 @@
 
 package com.android.keyguard;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
@@ -90,4 +93,11 @@
         mMessageAreaController.setIsVisible(true);
         verify(mKeyguardMessageArea).setIsVisible(true);
     }
+
+    @Test
+    public void testGetMessage() {
+        String msg = "abc";
+        when(mKeyguardMessageArea.getText()).thenReturn(msg);
+        assertThat(mMessageAreaController.getMessage()).isEqualTo(msg);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index b369098..ffd95f4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -31,6 +31,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
@@ -118,4 +119,14 @@
         keyguardPasswordViewController.startAppearAnimation()
         verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
     }
+
+    @Test
+    fun startAppearAnimation_withExistingMessage() {
+        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        keyguardPasswordViewController.startAppearAnimation()
+        verify(
+            mKeyguardMessageAreaController,
+            never()
+        ).setMessage(R.string.keyguard_enter_your_password)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 9eff704..b3d1c8f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -33,6 +33,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -112,4 +113,14 @@
         mKeyguardPatternViewController.startAppearAnimation()
         verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
     }
+
+    @Test
+    fun startAppearAnimation_withExistingMessage() {
+        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        mKeyguardPatternViewController.startAppearAnimation()
+        verify(
+            mKeyguardMessageAreaController,
+            never()
+        ).setMessage(R.string.keyguard_enter_your_password)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index d9efdea..8bcfe6f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -100,4 +100,12 @@
         pinViewController.startAppearAnimation()
         verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
     }
+
+    @Test
+    fun startAppearAnimation_withExistingMessage() {
+        Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        pinViewController.startAppearAnimation()
+        verify(keyguardMessageAreaController, Mockito.never())
+            .setMessage(R.string.keyguard_enter_your_password)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 1bd14e5..1d2b09c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -262,9 +262,12 @@
         ConstraintSet.Constraint userSwitcherConstraint =
                 getViewConstraint(R.id.keyguard_bouncer_user_switcher);
 
-        assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID);
+        assertThat(viewFlipperConstraint.layout.topToBottom).isEqualTo(
+                R.id.keyguard_bouncer_user_switcher);
         assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID);
         assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID);
+        assertThat(userSwitcherConstraint.layout.bottomToTop).isEqualTo(
+                mSecurityViewFlipper.getId());
         assertThat(userSwitcherConstraint.layout.topMargin).isEqualTo(
                 getContext().getResources().getDimensionPixelSize(
                         R.dimen.bouncer_user_switcher_y_trans));
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 ade83cf..6ba0634 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
@@ -18,6 +18,7 @@
 
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.doze.DozeHost
@@ -60,12 +61,12 @@
 
         underTest =
             KeyguardRepositoryImpl(
-                    statusBarStateController,
-                    dozeHost,
-                    wakefulnessLifecycle,
-                    biometricUnlockController,
-                    keyguardStateController,
-                    keyguardUpdateMonitor,
+                statusBarStateController,
+                dozeHost,
+                wakefulnessLifecycle,
+                biometricUnlockController,
+                keyguardStateController,
+                keyguardUpdateMonitor,
             )
     }
 
@@ -257,6 +258,48 @@
     }
 
     @Test
+    fun isKeyguardGoingAway() = runBlockingTest {
+        whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isKeyguardGoingAway.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardStateController.Callback>()
+        verify(keyguardStateController).addCallback(captor.capture())
+
+        whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(true)
+        captor.value.onKeyguardGoingAwayChanged()
+        assertThat(latest).isTrue()
+
+        whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false)
+        captor.value.onKeyguardGoingAwayChanged()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isDreaming() = runBlockingTest {
+        whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isDreaming.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(captor.capture())
+
+        captor.value.onDreamingStateChanged(true)
+        assertThat(latest).isTrue()
+
+        captor.value.onDreamingStateChanged(false)
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
     fun biometricUnlockState() = runBlockingTest {
         val values = mutableListOf<BiometricUnlockModel>()
         val job = underTest.biometricUnlockState.onEach(values::add).launchIn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 27d5d0a..2b03722 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -25,8 +25,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
-import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -38,7 +38,6 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.launchIn
@@ -91,18 +90,51 @@
                 }
             }
 
-            assertSteps(steps, listWithStep(BigDecimal(.1)))
+            assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)
 
             job.cancel()
             provider.stop()
         }
 
     @Test
-    fun `startTransition called during another transition fails`() {
-        underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null))
-        underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null))
+    fun `starting second transition will cancel the first transition`() {
+        runBlocking(IMMEDIATE) {
+            val (animator, provider) = setupAnimator(this)
 
-        assertThat(wtfHandler.failed).isTrue()
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
+            // 3 yields(), alternating with the animator, results in a value 0.1, which can be
+            // canceled and tested against
+            yield()
+            yield()
+            yield()
+
+            // Now start 2nd transition, which will interrupt the first
+            val job2 = underTest.transition(LOCKSCREEN, AOD).onEach { steps.add(it) }.launchIn(this)
+            val (animator2, provider2) = setupAnimator(this)
+            underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, AOD, animator2))
+
+            val startTime = System.currentTimeMillis()
+            while (animator2.isRunning()) {
+                yield()
+                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                }
+            }
+
+            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
+            assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
+
+            val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.9))
+            assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
+
+            job.cancel()
+            job2.cancel()
+            provider.stop()
+            provider2.stop()
+        }
     }
 
     @Test
@@ -165,11 +197,15 @@
         assertThat(wtfHandler.failed).isTrue()
     }
 
-    private fun listWithStep(step: BigDecimal): List<BigDecimal> {
+    private fun listWithStep(
+        step: BigDecimal,
+        start: BigDecimal = BigDecimal.ZERO,
+        stop: BigDecimal = BigDecimal.ONE,
+    ): List<BigDecimal> {
         val steps = mutableListOf<BigDecimal>()
 
-        var i = BigDecimal.ZERO
-        while (i.compareTo(BigDecimal.ONE) <= 0) {
+        var i = start
+        while (i.compareTo(stop) <= 0) {
             steps.add(i)
             i = (i + step).setScale(2, RoundingMode.HALF_UP)
         }
@@ -177,23 +213,43 @@
         return steps
     }
 
-    private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) {
+    private fun assertSteps(
+        steps: List<TransitionStep>,
+        fractions: List<BigDecimal>,
+        from: KeyguardState,
+        to: KeyguardState,
+    ) {
         assertThat(steps[0])
-            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME))
+            .isEqualTo(
+                TransitionStep(
+                    from,
+                    to,
+                    fractions[0].toFloat(),
+                    TransitionState.STARTED,
+                    OWNER_NAME
+                )
+            )
         fractions.forEachIndexed { index, fraction ->
             assertThat(steps[index + 1])
                 .isEqualTo(
                     TransitionStep(
-                        AOD,
-                        LOCKSCREEN,
+                        from,
+                        to,
                         fraction.toFloat(),
                         TransitionState.RUNNING,
                         OWNER_NAME
                     )
                 )
         }
+        val lastValue = fractions[fractions.size - 1].toFloat()
+        val status =
+            if (lastValue < 1f) {
+                TransitionState.CANCELED
+            } else {
+                TransitionState.FINISHED
+            }
         assertThat(steps[steps.size - 1])
-            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME))
+            .isEqualTo(TransitionStep(from, to, lastValue, status, OWNER_NAME))
 
         assertThat(wtfHandler.failed).isFalse()
     }
@@ -230,7 +286,7 @@
                 scope.launch {
                     frames.collect {
                         // Delay is required for AnimationHandler to properly register a callback
-                        delay(1)
+                        yield()
                         val (frameNumber, callback) = it
                         callback?.doFrame(frameNumber)
                     }
@@ -243,7 +299,7 @@
         }
 
         override fun postFrameCallback(cb: FrameCallback) {
-            frames.value = Pair(++frameCount, cb)
+            frames.value = Pair(frameCount++, cb)
         }
         override fun postCommitCallback(runnable: Runnable) {}
         override fun getFrameTime() = frameCount
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
index c85f7b9..21e5068 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -155,6 +155,7 @@
         mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN)
         verify(repository).setPrimaryVisible(false)
         verify(repository).setPrimaryShow(null)
+        verify(repository).setPrimaryHide(true)
         verify(falsingCollector).onBouncerHidden()
         verify(mPrimaryBouncerCallbackInteractor).dispatchReset()
         verify(mPrimaryBouncerCallbackInteractor).dispatchFullyHidden()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
new file mode 100644
index 0000000..0aa3621
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.screenrecord
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.widget.Spinner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class ScreenRecordPermissionDialogTest : SysuiTestCase() {
+
+    @Mock private lateinit var starter: ActivityStarter
+    @Mock private lateinit var controller: RecordingController
+    @Mock private lateinit var userContextProvider: UserContextProvider
+    @Mock private lateinit var flags: FeatureFlags
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var onStartRecordingClicked: Runnable
+
+    private lateinit var dialog: ScreenRecordPermissionDialog
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        dialog =
+            ScreenRecordPermissionDialog(
+                context,
+                controller,
+                starter,
+                dialogLaunchAnimator,
+                userContextProvider,
+                onStartRecordingClicked
+            )
+        dialog.onCreate(null)
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
+    }
+
+    @After
+    fun teardown() {
+        if (::dialog.isInitialized) {
+            dialog.dismiss()
+        }
+    }
+
+    @Test
+    fun testShowDialog_partialScreenSharingEnabled_optionsSpinnerIsVisible() {
+        dialog.show()
+
+        val visibility = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner).visibility
+        assertThat(visibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun testShowDialog_singleAppSelected_showTapsIsGone() {
+        dialog.show()
+        onSpinnerItemSelected(SINGLE_APP)
+
+        val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility
+        assertThat(visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testShowDialog_entireScreenSelected_showTapsIsVisible() {
+        dialog.show()
+        onSpinnerItemSelected(ENTIRE_SCREEN)
+
+        val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility
+        assertThat(visibility).isEqualTo(View.VISIBLE)
+    }
+
+    private fun onSpinnerItemSelected(position: Int) {
+        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
+        spinner.onItemSelectedListener.onItemSelected(spinner, mock(), position, /* id= */ 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 1f71e3c..7d2251e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -94,7 +94,6 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.doze.DozeLog;
@@ -492,7 +491,6 @@
                 mUnlockedScreenOffAnimationController,
                 mShadeTransitionController,
                 systemClock,
-                mock(CameraGestureHelper.class),
                 mKeyguardBottomAreaViewModel,
                 mKeyguardBottomAreaInteractor,
                 mDumpManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index c658593..77c690a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -1421,6 +1421,21 @@
     }
 
     @Test
+    public void onBiometricError_faceLockedOutSecondTimeOnBouncer_showsUnavailableMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
+
+        onFaceLockoutError("second lockout");
+
+        verify(mStatusBarKeyguardViewManager)
+                .setKeyguardMessage(
+                        eq(mContext.getString(R.string.keyguard_face_unlock_unavailable)),
+                        any());
+    }
+
+    @Test
     public void onBiometricError_faceLockedOutSecondTimeButUdfpsActive_showsNoMessage() {
         createController();
         onFaceLockoutError("first lockout");
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt
new file mode 100644
index 0000000..62b4e7b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.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.statusbar.connectivity
+
+import android.content.res.Resources
+import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
+
+typealias CarrierId = Int
+
+typealias NetworkType = String
+
+typealias ResId = Int
+
+class MobileIconCarrierIdOverridesFake : MobileIconCarrierIdOverrides {
+    /** Backing for [carrierIdEntryExists] */
+    var overriddenIds = mutableSetOf<Int>()
+
+    /** Backing for [getOverrideFor]. Map should be Map< CarrierId < NetworkType, ResId>> */
+    var overridesByCarrierId = mutableMapOf<CarrierId, Map<NetworkType, ResId>>()
+
+    override fun getOverrideFor(
+        carrierId: CarrierId,
+        networkType: NetworkType,
+        resources: Resources
+    ): ResId {
+        if (!overriddenIds.contains(carrierId)) return 0
+
+        return overridesByCarrierId[carrierId]?.get(networkType) ?: 0
+    }
+
+    override fun carrierIdEntryExists(carrierId: Int): Boolean {
+        return overriddenIds.contains(carrierId)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java
deleted file mode 100644
index 7ddfde3..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.connectivity;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-
-import com.android.settingslib.mobile.TelephonyIcons;
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class MobileStateTest extends SysuiTestCase {
-
-    private final MobileState mState = new MobileState();
-
-    @Before
-    public void setUp() {
-    }
-
-    @Test
-    public void testIsDataDisabledOrNotDefault_dataDisabled() {
-        mState.iconGroup = TelephonyIcons.DATA_DISABLED;
-        mState.userSetup = true;
-
-        assertTrue(mState.isDataDisabledOrNotDefault());
-    }
-
-    @Test
-    public void testIsDataDisabledOrNotDefault_notDefaultData() {
-        mState.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA;
-        mState.userSetup = true;
-
-        assertTrue(mState.isDataDisabledOrNotDefault());
-    }
-
-    @Test
-    public void testIsDataDisabledOrNotDefault_notDisabled() {
-        mState.iconGroup = TelephonyIcons.G;
-        mState.userSetup = true;
-
-        assertFalse(mState.isDataDisabledOrNotDefault());
-    }
-
-    @Test
-    public void testHasActivityIn_noData_noActivity() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityIn = false;
-
-        assertFalse(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityIn_noData_activityIn() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityIn = true;
-
-        assertFalse(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityIn_dataConnected_activityIn() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityIn = true;
-
-        assertTrue(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityIn_carrierNetworkChange() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = true;
-        mState.activityIn = true;
-
-        assertFalse(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityOut_noData_noActivity() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityOut = false;
-
-        assertFalse(mState.hasActivityOut());
-    }
-
-    @Test
-    public void testHasActivityOut_noData_activityOut() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityOut = true;
-
-        assertFalse(mState.hasActivityOut());
-    }
-
-    @Test
-    public void testHasActivityOut_dataConnected_activityOut() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityOut = true;
-
-        assertTrue(mState.hasActivityOut());
-    }
-
-    @Test
-    public void testHasActivityOut_carrierNetworkChange() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = true;
-        mState.activityOut = true;
-
-        assertFalse(mState.hasActivityOut());
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt
new file mode 100644
index 0000000..a226ded
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.connectivity
+
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MobileStateTest : SysuiTestCase() {
+
+    private val state = MobileState()
+    @Before fun setUp() {}
+
+    @Test
+    fun testIsDataDisabledOrNotDefault_dataDisabled() {
+        state.iconGroup = TelephonyIcons.DATA_DISABLED
+        state.userSetup = true
+        assertTrue(state.isDataDisabledOrNotDefault)
+    }
+
+    @Test
+    fun testIsDataDisabledOrNotDefault_notDefaultData() {
+        state.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA
+        state.userSetup = true
+        assertTrue(state.isDataDisabledOrNotDefault)
+    }
+
+    @Test
+    fun testIsDataDisabledOrNotDefault_notDisabled() {
+        state.iconGroup = TelephonyIcons.G
+        state.userSetup = true
+        assertFalse(state.isDataDisabledOrNotDefault)
+    }
+
+    @Test
+    fun testHasActivityIn_noData_noActivity() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityIn = false
+        assertFalse(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityIn_noData_activityIn() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityIn = true
+        assertFalse(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityIn_dataConnected_activityIn() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = false
+        state.activityIn = true
+        assertTrue(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityIn_carrierNetworkChange() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = true
+        state.activityIn = true
+        assertFalse(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityOut_noData_noActivity() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityOut = false
+        assertFalse(state.hasActivityOut())
+    }
+
+    @Test
+    fun testHasActivityOut_noData_activityOut() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityOut = true
+        assertFalse(state.hasActivityOut())
+    }
+
+    @Test
+    fun testHasActivityOut_dataConnected_activityOut() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = false
+        state.activityOut = true
+        assertTrue(state.hasActivityOut())
+    }
+
+    @Test
+    fun testHasActivityOut_carrierNetworkChange() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = true
+        state.activityOut = true
+        assertFalse(state.hasActivityOut())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index 9c65fac..9c870b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -71,6 +71,7 @@
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -125,6 +126,8 @@
     protected CarrierConfigTracker mCarrierConfigTracker;
     protected FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     protected Handler mMainHandler;
+    // Use a real mobile mappings object since lots of tests rely on it
+    protected FakeMobileMappingsProxy mMobileMappingsProxy = new FakeMobileMappingsProxy();
     protected WifiStatusTrackerFactory mWifiStatusTrackerFactory;
     protected MobileSignalControllerFactory mMobileFactory;
 
@@ -219,10 +222,13 @@
 
         mWifiStatusTrackerFactory = new WifiStatusTrackerFactory(
                 mContext, mMockWm, mMockNsm, mMockCm, mMainHandler);
+        // Most of these tests rely on the actual MobileMappings behavior
+        mMobileMappingsProxy.setUseRealImpl(true);
         mMobileFactory = new MobileSignalControllerFactory(
                 mContext,
                 mCallbackHandler,
-                mCarrierConfigTracker
+                mCarrierConfigTracker,
+                mMobileMappingsProxy
         );
 
         mNetworkController = new NetworkControllerImpl(mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index 4bed4a1..1d11226 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -18,12 +18,21 @@
 
 import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
 import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED;
+import static android.telephony.TelephonyManager.EXTRA_CARRIER_ID;
+import static android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID;
 
+import static com.android.settingslib.mobile.TelephonyIcons.NR_5G_PLUS;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.content.Intent;
 import android.net.NetworkCapabilities;
 import android.os.Handler;
 import android.os.Looper;
@@ -35,6 +44,7 @@
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
+import com.android.settingslib.SignalIcon.MobileIconGroup;
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.dump.DumpManager;
@@ -45,6 +55,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.HashMap;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
@@ -329,6 +341,57 @@
         assertFalse(mNetworkController.isMobileDataNetworkInService());
     }
 
+    @Test
+    public void mobileSignalController_getsCarrierId() {
+        when(mMockTm.getSimCarrierId()).thenReturn(1);
+        setupDefaultSignal();
+
+        assertEquals(1, mMobileSignalController.getState().getCarrierId());
+    }
+
+    @Test
+    public void mobileSignalController_updatesCarrierId_onChange() {
+        when(mMockTm.getSimCarrierId()).thenReturn(1);
+        setupDefaultSignal();
+
+        // Updates are sent down through this broadcast, we can send the intent directly
+        Intent intent = new Intent(ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
+        intent.putExtra(EXTRA_SUBSCRIPTION_ID, mSubId);
+        intent.putExtra(EXTRA_CARRIER_ID, 2);
+
+        mMobileSignalController.handleBroadcast(intent);
+
+        assertEquals(2, mMobileSignalController.getState().getCarrierId());
+    }
+
+    @Test
+    public void networkTypeIcon_hasCarrierIdOverride() {
+        int fakeCarrier = 1;
+        int fakeIconOverride = 12345;
+        int testDataNetType = 100;
+        String testDataString = "100";
+        HashMap<String, MobileIconGroup> testMap = new HashMap<>();
+        testMap.put(testDataString, NR_5G_PLUS);
+
+        // Pretend that there is an override for this icon, and this carrier ID
+        NetworkTypeResIdCache mockCache = mock(NetworkTypeResIdCache.class);
+        when(mockCache.get(eq(NR_5G_PLUS), eq(fakeCarrier), any())).thenReturn(fakeIconOverride);
+
+        // Turn off the default mobile mapping, so we can override
+        mMobileMappingsProxy.setUseRealImpl(false);
+        mMobileMappingsProxy.setIconMap(testMap);
+        // Use the mocked cache
+        mMobileSignalController.mCurrentState.setNetworkTypeResIdCache(mockCache);
+        // Rebuild the network map
+        mMobileSignalController.setConfiguration(mConfig);
+        when(mMockTm.getSimCarrierId()).thenReturn(fakeCarrier);
+
+        setupDefaultSignal();
+        updateDataConnectionState(TelephonyManager.DATA_CONNECTED, testDataNetType);
+
+        verifyDataIndicators(fakeIconOverride);
+    }
+
     private void testDataActivity(int direction, boolean in, boolean out) {
         updateDataActivity(direction);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt
new file mode 100644
index 0000000..9e73487
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.connectivity
+
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NetworkTypeResIdCacheTest : SysuiTestCase() {
+    private lateinit var cache: NetworkTypeResIdCache
+    private var overrides = MobileIconCarrierIdOverridesFake()
+
+    @Before
+    fun setUp() {
+        cache = NetworkTypeResIdCache(overrides)
+    }
+
+    @Test
+    fun carrier1_noOverride_usesDefault() {
+        assertThat(cache.get(group1, CARRIER_1, context)).isEqualTo(iconDefault1)
+    }
+
+    @Test
+    fun carrier1_overridden_usesOverride() {
+        overrides.overriddenIds.add(CARRIER_1)
+        overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1)
+
+        assertThat(cache.get(group1, CARRIER_1, context)).isEqualTo(iconOverride1)
+    }
+
+    @Test
+    fun carrier1_override_carrier2UsesDefault() {
+        overrides.overriddenIds.add(CARRIER_1)
+        overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1)
+
+        assertThat(cache.get(group1, CARRIER_2, context)).isEqualTo(iconDefault1)
+    }
+
+    @Test
+    fun carrier1_overrideType1_type2UsesDefault() {
+        overrides.overriddenIds.add(CARRIER_1)
+        overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1)
+
+        assertThat(cache.get(group2, CARRIER_1, context)).isEqualTo(iconDefault2)
+    }
+
+    companion object {
+        // Simplified icon overrides here
+        const val CARRIER_1 = 1
+        const val CARRIER_2 = 2
+
+        const val NET_TYPE_1 = "one"
+        const val iconDefault1 = 123
+        const val iconOverride1 = 321
+        val group1 = MobileIconGroup(NET_TYPE_1, /* dataContentDesc */ 0, iconDefault1)
+
+        const val NET_TYPE_2 = "two"
+        const val iconDefault2 = 234
+
+        val group2 = MobileIconGroup(NET_TYPE_2, /* dataContentDesc*/ 0, iconDefault2)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index 5ebaf69..d5bfe1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -41,6 +41,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -60,6 +61,8 @@
 
 import java.util.Optional;
 
+import dagger.Lazy;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
@@ -84,6 +87,7 @@
     @Mock private Vibrator mVibrator;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private SystemBarAttributesListener mSystemBarAttributesListener;
+    @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -115,7 +119,8 @@
                 Optional.of(mVibrator),
                 new DisableFlagsLogger(),
                 DEFAULT_DISPLAY,
-                mSystemBarAttributesListener);
+                mSystemBarAttributesListener,
+                mCameraLauncherLazy);
 
         when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
         when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 5ad1431..41912f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -112,6 +112,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelView;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
@@ -288,6 +289,8 @@
     @Mock private InteractionJankMonitor mJankMonitor;
     @Mock private DeviceStateManager mDeviceStateManager;
     @Mock private WiredChargingRippleController mWiredChargingRippleController;
+    @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
+    @Mock private CameraLauncher mCameraLauncher;
     /**
      * The process of registering/unregistering a predictive back callback requires a
      * ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test.
@@ -380,6 +383,7 @@
 
         when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper);
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
+        when(mCameraLauncherLazy.get()).thenReturn(mCameraLauncher);
 
         when(mStatusBarComponentFactory.create()).thenReturn(mCentralSurfacesComponent);
         when(mCentralSurfacesComponent.getNotificationShadeWindowViewController()).thenReturn(
@@ -481,7 +485,9 @@
                 mActivityLaunchAnimator,
                 mJankMonitor,
                 mDeviceStateManager,
-                mWiredChargingRippleController, mDreamManager) {
+                mWiredChargingRippleController,
+                mDreamManager,
+                mCameraLauncherLazy) {
             @Override
             protected ViewRootImpl getViewRootImpl() {
                 return mViewRootImpl;
@@ -893,7 +899,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
-        when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(true);
+        when(mCameraLauncher.isLaunchingAffordance()).thenReturn(true);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
     }
@@ -929,7 +935,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
-        when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(false);
+        when(mCameraLauncher.isLaunchingAffordance()).thenReturn(false);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index df48e1d..808abc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -1437,6 +1437,17 @@
     }
 
     @Test
+    public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() {
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
+        mScrimController.setClipsQsScrim(false);
+
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        finishAnimationsImmediately();
+        assertThat(mScrimBehind.getTint())
+                .isEqualTo(ScrimState.KEYGUARD.getBehindTint());
+    }
+
+    @Test
     public void testNotificationTransparency_followsTransitionToFullShade() {
         mScrimController.transitionTo(SHADE_LOCKED);
         mScrimController.setRawPanelExpansionFraction(1.0f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
index 6d8d902..a052008 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -16,31 +16,59 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.util
 
+import android.telephony.TelephonyDisplayInfo
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings.Config
 import com.android.settingslib.mobile.TelephonyIcons
 
 class FakeMobileMappingsProxy : MobileMappingsProxy {
+    // The old [NetworkControllerDataTest] infra requires us to be able to use the real
+    // impl sometimes
+    var useRealImpl = false
+
+    private var realImpl = MobileMappingsProxyImpl()
     private var iconMap = mapOf<String, MobileIconGroup>()
     private var defaultIcons = TelephonyIcons.THREE_G
 
     fun setIconMap(map: Map<String, MobileIconGroup>) {
         iconMap = map
     }
-    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> {
+        if (useRealImpl) {
+            return realImpl.mapIconSets(config)
+        }
+        return iconMap
+    }
     fun getIconMap() = iconMap
 
     fun setDefaultIcons(group: MobileIconGroup) {
         defaultIcons = group
     }
-    override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons
+    override fun getDefaultIcons(config: Config): MobileIconGroup {
+        if (useRealImpl) {
+            return realImpl.getDefaultIcons(config)
+        }
+        return defaultIcons
+    }
+
+    /** This is only used in the old pipeline, use the real impl always */
+    override fun getIconKey(displayInfo: TelephonyDisplayInfo): String {
+        return realImpl.getIconKey(displayInfo)
+    }
+
     fun getDefaultIcons(): MobileIconGroup = defaultIcons
 
     override fun toIconKey(networkType: Int): String {
+        if (useRealImpl) {
+            return realImpl.toIconKeyOverride(networkType)
+        }
         return networkType.toString()
     }
 
     override fun toIconKeyOverride(networkType: Int): String {
+        if (useRealImpl) {
+            return realImpl.toIconKeyOverride(networkType)
+        }
         return toIconKey(networkType) + "_override"
     }
 }
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 6f70f0e..a798f40 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,9 @@
     private val _isDozing = MutableStateFlow(false)
     override val isDozing: Flow<Boolean> = _isDozing
 
+    private val _isDreaming = MutableStateFlow(false)
+    override val isDreaming: Flow<Boolean> = _isDreaming
+
     private val _dozeAmount = MutableStateFlow(0f)
     override val dozeAmount: Flow<Float> = _dozeAmount
 
@@ -54,10 +57,13 @@
     override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState
 
     private val _isUdfpsSupported = MutableStateFlow(false)
-    
+
     private val _isBouncerShowing = MutableStateFlow(false)
     override val isBouncerShowing: Flow<Boolean> = _isBouncerShowing
 
+    private val _isKeyguardGoingAway = MutableStateFlow(false)
+    override val isKeyguardGoingAway: Flow<Boolean> = _isKeyguardGoingAway
+
     private val _biometricUnlockState = MutableStateFlow(BiometricUnlockModel.NONE)
     override val biometricUnlockState: Flow<BiometricUnlockModel> = _biometricUnlockState
 
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 064282d..d255669 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1782,7 +1782,7 @@
     }
 
     @Override
-    public List<UserHandle> getVisibleUsers() {
+    public int[] getVisibleUsers() {
         if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) {
             throw new SecurityException("Caller needs MANAGE_USERS or INTERACT_ACROSS_USERS "
                     + "permission to get list of visible users");
@@ -1790,18 +1790,19 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             // TODO(b/2399825580): refactor into UserDisplayAssigner
+            IntArray visibleUsers;
             synchronized (mUsersLock) {
                 int usersSize = mUsers.size();
-                ArrayList<UserHandle> visibleUsers = new ArrayList<>(usersSize);
+                visibleUsers = new IntArray();
                 for (int i = 0; i < usersSize; i++) {
                     UserInfo ui = mUsers.valueAt(i).info;
                     if (!ui.partial && !ui.preCreated && !mRemovingUserIds.get(ui.id)
                             && mUserVisibilityMediator.isUserVisible(ui.id)) {
-                        visibleUsers.add(UserHandle.of(ui.id));
+                        visibleUsers.add(ui.id);
                     }
                 }
-                return visibleUsers;
             }
+            return visibleUsers.toArray();
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 0015fad..bd81062 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -16,8 +16,8 @@
 package com.android.server.pm;
 
 import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID;
-import static android.os.UserHandle.USER_CURRENT;
 import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.annotation.IntDef;
@@ -54,10 +54,9 @@
 
     private static final String PREFIX_START_USER_RESULT = "START_USER_";
 
-    // NOTE: it's set as USER_CURRENT instead of USER_NULL because NO_PROFILE_GROUP_ID has the same
-    // falue of USER_NULL, which would complicate some checks (especially on unit tests)
+    // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices
     @VisibleForTesting
-    static final int INITIAL_CURRENT_USER_ID = USER_CURRENT;
+    static final int INITIAL_CURRENT_USER_ID = USER_SYSTEM;
 
     public static final int START_USER_RESULT_SUCCESS_VISIBLE = 1;
     public static final int START_USER_RESULT_SUCCESS_INVISIBLE = 2;
@@ -126,7 +125,8 @@
                 }
                 if (foreground) {
                     Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user in "
-                            + "foreground");
+                            + "foreground", userId, actualProfileGroupId, foreground,
+                            displayId);
                     return START_USER_RESULT_FAILURE;
                 } else {
                     boolean isParentRunning = mStartedProfileGroupIds
diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
index d0c381e..21b241a 100644
--- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
+++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
@@ -56,8 +56,11 @@
     private static final int SURFACE_FLINGER_CALLBACK_WINDOWS_STABLE_TIMES_MS = 35;
     // To avoid the surface flinger callbacks always comes within in 2 frames, then no windows
     // are reported to the A11y framework, and the animation duration time is 500ms, so setting
-    // this value as the max timeout value to force computing changed windows.
-    private static final int WINDOWS_CHANGED_NOTIFICATION_MAX_DURATION_TIMES_MS = 500;
+    // this value as the max timeout value to force computing changed windows. However, since
+    // UiAutomator waits 500ms to determine that things are idle. Since we aren't actually idle,
+    // we need to reduce the timeout here a little so that we can deliver an updated state before
+    // UiAutomator reports idle based-on stale information.
+    private static final int WINDOWS_CHANGED_NOTIFICATION_MAX_DURATION_TIMES_MS = 450;
 
     private static final float[] sTempFloats = new float[9];
 
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index f138311..808130a 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -155,7 +155,18 @@
     }
 
     @Test
-    public void testWritingTwoFilesToDisk() throws Exception {
+    public void testWritingTwoJobsToDisk_singleFile() throws Exception {
+        mTaskStoreUnderTest.setUseSplitFiles(false);
+        runWritingTwoJobsToDisk();
+    }
+
+    @Test
+    public void testWritingTwoJobsToDisk_splitFiles() throws Exception {
+        mTaskStoreUnderTest.setUseSplitFiles(true);
+        runWritingTwoJobsToDisk();
+    }
+
+    private void runWritingTwoJobsToDisk() throws Exception {
         final JobInfo task1 = new Builder(8, mComponent)
                 .setRequiresDeviceIdle(true)
                 .setPeriodic(10000L)
@@ -169,8 +180,10 @@
                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                 .setPersisted(true)
                 .build();
-        final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, SOME_UID, null, -1, null);
-        final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, SOME_UID, null, -1, null);
+        final int uid1 = SOME_UID;
+        final int uid2 = uid1 + 1;
+        final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
+        final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
         mTaskStoreUnderTest.add(taskStatus1);
         mTaskStoreUnderTest.add(taskStatus2);
         waitForPendingIo();
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index 73aff43..a834e2bb 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -468,14 +468,14 @@
         final boolean isQosBearerSessionsSame =
                 (mQosBearerSessions == null || other.mQosBearerSessions == null)
                 ? mQosBearerSessions == other.mQosBearerSessions
-                : mQosBearerSessions.size() == other.mQosBearerSessions.size()
-                && mQosBearerSessions.containsAll(other.mQosBearerSessions);
+                : (mQosBearerSessions.size() == other.mQosBearerSessions.size()
+                        && mQosBearerSessions.containsAll(other.mQosBearerSessions));
 
         final boolean isTrafficDescriptorsSame =
                 (mTrafficDescriptors == null || other.mTrafficDescriptors == null)
                 ? mTrafficDescriptors == other.mTrafficDescriptors
-                : mTrafficDescriptors.size() == other.mTrafficDescriptors.size()
-                && mTrafficDescriptors.containsAll(other.mTrafficDescriptors);
+                : (mTrafficDescriptors.size() == other.mTrafficDescriptors.size()
+                        && mTrafficDescriptors.containsAll(other.mTrafficDescriptors));
 
         return mCause == other.mCause
                 && mSuggestedRetryTime == other.mSuggestedRetryTime
@@ -504,10 +504,35 @@
 
     @Override
     public int hashCode() {
+        // Generate order-independent hashes for lists
+        int addressesHash = mAddresses.stream()
+                .map(LinkAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int dnsAddressesHash = mDnsAddresses.stream()
+                .map(InetAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int gatewayAddressesHash = mGatewayAddresses.stream()
+                .map(InetAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int pcscfAddressesHash = mPcscfAddresses.stream()
+                .map(InetAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int qosBearerSessionsHash = mQosBearerSessions.stream()
+                .map(QosBearerSession::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int trafficDescriptorsHash = mTrafficDescriptors.stream()
+                .map(TrafficDescriptor::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
         return Objects.hash(mCause, mSuggestedRetryTime, mId, mLinkStatus, mProtocolType,
-                mInterfaceName, mAddresses, mDnsAddresses, mGatewayAddresses, mPcscfAddresses,
-                mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId, mDefaultQos,
-                mQosBearerSessions, mSliceInfo, mTrafficDescriptors);
+                mInterfaceName, addressesHash, dnsAddressesHash, gatewayAddressesHash,
+                pcscfAddressesHash, mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId,
+                mDefaultQos, qosBearerSessionsHash, mSliceInfo, trafficDescriptorsHash);
     }
 
     @Override
@@ -816,8 +841,8 @@
         /**
          * Set pdu session id.
          * <p/>
-         * The id must be between 1 and 15 when linked to a pdu session.  If no pdu session
-         * exists for the current data call, the id must be set to {@link PDU_SESSION_ID_NOT_SET}.
+         * The id must be between 1 and 15 when linked to a pdu session. If no pdu session
+         * exists for the current data call, the id must be set to {@link #PDU_SESSION_ID_NOT_SET}.
          *
          * @param pduSessionId Pdu Session Id of the data call.
          * @return The same instance of the builder.
@@ -858,6 +883,7 @@
          */
         public @NonNull Builder setQosBearerSessions(
                 @NonNull List<QosBearerSession> qosBearerSessions) {
+            Objects.requireNonNull(qosBearerSessions);
             mQosBearerSessions = qosBearerSessions;
             return this;
         }
@@ -891,6 +917,7 @@
          */
         public @NonNull Builder setTrafficDescriptors(
                 @NonNull List<TrafficDescriptor> trafficDescriptors) {
+            Objects.requireNonNull(trafficDescriptors);
             mTrafficDescriptors = trafficDescriptors;
             return this;
         }
diff --git a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
index dd9b294..afaeca1 100644
--- a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
+++ b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
@@ -15,7 +15,6 @@
  */
 package com.android.frameworks.perftests.job;
 
-
 import android.app.job.JobInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -46,7 +45,8 @@
 public class JobStorePerfTests {
     private static final String SOURCE_PACKAGE = "com.android.frameworks.perftests.job";
     private static final int SOURCE_USER_ID = 0;
-    private static final int CALLING_UID = 10079;
+    private static final int BASE_CALLING_UID = 10079;
+    private static final int MAX_UID_COUNT = 10;
 
     private static Context sContext;
     private static File sTestDir;
@@ -65,10 +65,10 @@
         sJobStore = JobStore.initAndGetForTesting(sContext, sTestDir);
 
         for (int i = 0; i < 50; i++) {
-            sFewJobs.add(createJobStatus("fewJobs", i));
+            sFewJobs.add(createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % MAX_UID_COUNT)));
         }
         for (int i = 0; i < 500; i++) {
-            sManyJobs.add(createJobStatus("manyJobs", i));
+            sManyJobs.add(createJobStatus("manyJobs", i, BASE_CALLING_UID + (i % MAX_UID_COUNT)));
         }
     }
 
@@ -104,6 +104,64 @@
         runPersistedJobWriting(sManyJobs);
     }
 
+    private void runPersistedJobWriting_delta(List<JobStatus> jobList,
+            List<JobStatus> jobAdditions, List<JobStatus> jobRemovals) {
+        final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState();
+
+        long elapsedTimeNs = 0;
+        while (benchmarkState.keepRunning(elapsedTimeNs)) {
+            sJobStore.clearForTesting();
+            for (JobStatus job : jobList) {
+                sJobStore.addForTesting(job);
+            }
+            sJobStore.writeStatusToDiskForTesting();
+
+            for (JobStatus job : jobAdditions) {
+                sJobStore.addForTesting(job);
+            }
+            for (JobStatus job : jobRemovals) {
+                sJobStore.removeForTesting(job);
+            }
+
+            final long startTime = SystemClock.elapsedRealtimeNanos();
+            sJobStore.writeStatusToDiskForTesting();
+            final long endTime = SystemClock.elapsedRealtimeNanos();
+            elapsedTimeNs = endTime - startTime;
+        }
+    }
+
+    @Test
+    public void testPersistedJobWriting_delta_fewJobs() {
+        List<JobStatus> additions = new ArrayList<>();
+        List<JobStatus> removals = new ArrayList<>();
+        final int numModifiedUids = MAX_UID_COUNT / 2;
+        for (int i = 0; i < sFewJobs.size() / 3; ++i) {
+            JobStatus job = createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % numModifiedUids));
+            if (i % 2 == 0) {
+                additions.add(job);
+            } else {
+                removals.add(job);
+            }
+        }
+        runPersistedJobWriting_delta(sFewJobs, additions, removals);
+    }
+
+    @Test
+    public void testPersistedJobWriting_delta_manyJobs() {
+        List<JobStatus> additions = new ArrayList<>();
+        List<JobStatus> removals = new ArrayList<>();
+        final int numModifiedUids = MAX_UID_COUNT / 2;
+        for (int i = 0; i < sManyJobs.size() / 3; ++i) {
+            JobStatus job = createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % numModifiedUids));
+            if (i % 2 == 0) {
+                additions.add(job);
+            } else {
+                removals.add(job);
+            }
+        }
+        runPersistedJobWriting_delta(sManyJobs, additions, removals);
+    }
+
     private void runPersistedJobReading(List<JobStatus> jobList, boolean rtcIsGood) {
         final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState();
 
@@ -144,12 +202,12 @@
         runPersistedJobReading(sManyJobs, false);
     }
 
-    private static JobStatus createJobStatus(String testTag, int jobId) {
+    private static JobStatus createJobStatus(String testTag, int jobId, int callingUid) {
         JobInfo jobInfo = new JobInfo.Builder(jobId,
                 new ComponentName(sContext, "JobStorePerfTestJobService"))
                 .setPersisted(true)
                 .build();
         return JobStatus.createFromJobInfo(
-                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
     }
 }