Merge "Fix NPEs in DataCallResponse"
diff --git a/apct-tests/perftests/multiuser/AndroidManifest.xml b/apct-tests/perftests/multiuser/AndroidManifest.xml
index 63e5983..5befa1f 100644
--- a/apct-tests/perftests/multiuser/AndroidManifest.xml
+++ b/apct-tests/perftests/multiuser/AndroidManifest.xml
@@ -35,4 +35,8 @@
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.perftests.multiuser"/>
 
+    <queries>
+        <package android:name="perftests.multiuser.apps.dummyapp" />
+    </queries>
+
 </manifest>
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 05c9012..756c09f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -41722,7 +41722,7 @@
     field public static final String KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY = "gsm_roaming_networks_string_array";
     field public static final String KEY_HAS_IN_CALL_NOISE_SUPPRESSION_BOOL = "has_in_call_noise_suppression_bool";
     field public static final String KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL = "hide_carrier_network_settings_bool";
-    field public static final String KEY_HIDE_ENABLE_2G = "hide_enable_2g_bool";
+    field @Deprecated public static final String KEY_HIDE_ENABLE_2G = "hide_enable_2g_bool";
     field public static final String KEY_HIDE_ENHANCED_4G_LTE_BOOL = "hide_enhanced_4g_lte_bool";
     field public static final String KEY_HIDE_IMS_APN_BOOL = "hide_ims_apn_bool";
     field public static final String KEY_HIDE_LTE_PLUS_DATA_ICON_BOOL = "hide_lte_plus_data_icon_bool";
@@ -44095,8 +44095,8 @@
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR = 8; // 0x8
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13; // 0xd
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa
-    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; // 0xd
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB = 14; // 0xe
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 2012948..8ec313ec 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -30,10 +30,12 @@
 per-file Service* = file:/services/core/java/com/android/server/am/OWNERS
 per-file SystemServiceRegistry.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file *UserSwitchObserver* = file:/services/core/java/com/android/server/am/OWNERS
-per-file UiAutomation* = file:/services/accessibility/OWNERS
+per-file *UiAutomation* = file:/services/accessibility/OWNERS
 per-file GameManager* = file:/GAME_MANAGER_OWNERS
+per-file GameMode* = file:/GAME_MANAGER_OWNERS
 per-file GameState* = file:/GAME_MANAGER_OWNERS
 per-file IGameManager* = file:/GAME_MANAGER_OWNERS
+per-file IGameMode* = file:/GAME_MANAGER_OWNERS
 
 # ActivityThread
 per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java
index 30a6c31..ee14708 100644
--- a/core/java/android/app/servertransaction/ClientTransaction.java
+++ b/core/java/android/app/servertransaction/ClientTransaction.java
@@ -176,7 +176,6 @@
     /** Write to Parcel. */
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeStrongBinder(mClient.asBinder());
         final boolean writeActivityToken = mActivityToken != null;
         dest.writeBoolean(writeActivityToken);
         if (writeActivityToken) {
@@ -192,7 +191,6 @@
 
     /** Read from Parcel. */
     private ClientTransaction(Parcel in) {
-        mClient = (IApplicationThread) in.readStrongBinder();
         final boolean readActivityToken = in.readBoolean();
         if (readActivityToken) {
             mActivityToken = in.readStrongBinder();
diff --git a/core/java/android/content/pm/UserPackage.java b/core/java/android/content/pm/UserPackage.java
index e75f551..7ca92c3 100644
--- a/core/java/android/content/pm/UserPackage.java
+++ b/core/java/android/content/pm/UserPackage.java
@@ -33,17 +33,19 @@
  * @hide
  */
 public final class UserPackage {
+    private static final boolean ENABLE_CACHING = true;
+
     @UserIdInt
     public final int userId;
     public final String packageName;
 
-    @GuardedBy("sCache")
+    private static final Object sCacheLock = new Object();
+    @GuardedBy("sCacheLock")
     private static final SparseArrayMap<String, UserPackage> sCache = new SparseArrayMap<>();
 
-    private static final Object sUserIdLock = new Object();
     private static final class NoPreloadHolder {
         /** Set of userIDs to cache objects for. */
-        @GuardedBy("sUserIdLock")
+        @GuardedBy("sCacheLock")
         private static int[] sUserIds = new int[]{UserHandle.getUserId(Process.myUid())};
     }
 
@@ -80,13 +82,16 @@
     /** Return an instance of this class representing the given userId + packageName combination. */
     @NonNull
     public static UserPackage of(@UserIdInt int userId, @NonNull String packageName) {
-        synchronized (sUserIdLock) {
+        if (!ENABLE_CACHING) {
+            return new UserPackage(userId, packageName);
+        }
+
+        synchronized (sCacheLock) {
             if (!ArrayUtils.contains(NoPreloadHolder.sUserIds, userId)) {
                 // Don't cache objects for invalid userIds.
                 return new UserPackage(userId, packageName);
             }
-        }
-        synchronized (sCache) {
+
             UserPackage up = sCache.get(userId, packageName);
             if (up == null) {
                 packageName = packageName.intern();
@@ -99,23 +104,27 @@
 
     /** Remove the specified app from the cache. */
     public static void removeFromCache(@UserIdInt int userId, @NonNull String packageName) {
-        synchronized (sCache) {
+        if (!ENABLE_CACHING) {
+            return;
+        }
+
+        synchronized (sCacheLock) {
             sCache.delete(userId, packageName);
         }
     }
 
     /** Indicate the list of valid user IDs on the device. */
     public static void setValidUserIds(@NonNull int[] userIds) {
-        userIds = userIds.clone();
-        synchronized (sUserIdLock) {
-            NoPreloadHolder.sUserIds = userIds;
+        if (!ENABLE_CACHING) {
+            return;
         }
-        synchronized (sCache) {
+
+        userIds = userIds.clone();
+        synchronized (sCacheLock) {
+            NoPreloadHolder.sUserIds = userIds;
+
             for (int u = sCache.numMaps() - 1; u >= 0; --u) {
                 final int userId = sCache.keyAt(u);
-                // Not holding sUserIdLock is intentional here. We don't modify the elements within
-                // the array and so even if this method is called multiple times with different sets
-                // of user IDs, we want to adjust the cache based on each new array.
                 if (!ArrayUtils.contains(userIds, userId)) {
                     sCache.deleteAt(u);
                 }
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index c6a3725..8759a6a 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -828,8 +828,7 @@
     }
 
     /**
-     * Invokes {@code IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
-     * CharSequence, TextAttribute)}.
+     * Replaces the specific range in the current input field with suggested text.
      *
      * @param start the character index where the replacement should start.
      * @param end the character index where the replacement should end.
@@ -839,8 +838,9 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
-     * @return {@code true} if the invocation is completed without {@link RemoteException}, {@code
-     *     false} otherwise.
+     * @return {@code true} if the specific range is replaced successfully, {@code false} otherwise.
+     * @see android.view.inputmethod.InputConnection#replaceText(int, int, CharSequence, int,
+     *     TextAttribute)
      */
     @AnyThread
     public boolean replaceText(
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 47b16a3..d2a4ae2 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -55,6 +55,20 @@
             "android.service.controls.ControlsProviderService";
 
     /**
+     * Manifest metadata to show a custom embedded activity as part of device controls.
+     *
+     * The value of this metadata must be the {@link ComponentName} as a string of an activity in
+     * the same package that will be launched as part of a TaskView.
+     *
+     * The activity must be exported, enabled and protected by
+     * {@link Manifest.permission.BIND_CONTROLS}.
+     *
+     * @hide
+     */
+    public static final String META_DATA_PANEL_ACTIVITY =
+            "android.service.controls.META_DATA_PANEL_ACTIVITY";
+
+    /**
      * @hide
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 37fc9f2..104570d0 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -575,6 +575,7 @@
          */
         public void reportEngineShown(boolean waitForEngineShown) {
             if (mIWallpaperEngine.mShownReported) return;
+            Trace.beginSection("WPMS.reportEngineShown-" + waitForEngineShown);
             Log.d(TAG, "reportEngineShown: shouldWait=" + waitForEngineShown);
             if (!waitForEngineShown) {
                 Message message = mCaller.obtainMessage(MSG_REPORT_SHOWN);
@@ -587,6 +588,7 @@
                     mCaller.sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(5));
                 }
             }
+            Trace.endSection();
         }
 
         /**
@@ -1259,7 +1261,9 @@
                             didSurface = true;
                             if (DEBUG) Log.v(TAG, "onSurfaceCreated("
                                     + mSurfaceHolder + "): " + this);
+                            Trace.beginSection("WPMS.Engine.onSurfaceCreated");
                             onSurfaceCreated(mSurfaceHolder);
+                            Trace.endSection();
                             SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                             if (callbacks != null) {
                                 for (SurfaceHolder.Callback c : callbacks) {
@@ -1285,8 +1289,10 @@
                                     + ", " + mCurWidth + ", " + mCurHeight
                                     + "): " + this);
                             didSurface = true;
+                            Trace.beginSection("WPMS.Engine.onSurfaceChanged");
                             onSurfaceChanged(mSurfaceHolder, mFormat,
                                     mCurWidth, mCurHeight);
+                            Trace.endSection();
                             SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                             if (callbacks != null) {
                                 for (SurfaceHolder.Callback c : callbacks) {
@@ -1303,11 +1309,15 @@
                             if (DEBUG) {
                                 Log.v(TAG, "dispatching insets=" + windowInsets);
                             }
+                            Trace.beginSection("WPMS.Engine.onApplyWindowInsets");
                             onApplyWindowInsets(windowInsets);
+                            Trace.endSection();
                         }
 
                         if (redrawNeeded) {
+                            Trace.beginSection("WPMS.Engine.onSurfaceRedrawNeeded");
                             onSurfaceRedrawNeeded(mSurfaceHolder);
+                            Trace.endSection();
                             SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                             if (callbacks != null) {
                                 for (SurfaceHolder.Callback c : callbacks) {
@@ -1332,11 +1342,15 @@
                                 // the state to get them to notice.
                                 if (DEBUG) Log.v(TAG, "onVisibilityChanged(true) at surface: "
                                         + this);
+                                Trace.beginSection("WPMS.Engine.onVisibilityChanged-true");
                                 onVisibilityChanged(true);
+                                Trace.endSection();
                             }
                             if (DEBUG) Log.v(TAG, "onVisibilityChanged(false) at surface: "
                                         + this);
+                            Trace.beginSection("WPMS.Engine.onVisibilityChanged-false");
                             onVisibilityChanged(false);
+                            Trace.endSection();
                         }
                     } finally {
                         mIsCreating = false;
@@ -1421,12 +1435,16 @@
             mDisplayState = mDisplay.getState();
 
             if (DEBUG) Log.v(TAG, "onCreate(): " + this);
+            Trace.beginSection("WPMS.Engine.onCreate");
             onCreate(mSurfaceHolder);
+            Trace.endSection();
 
             mInitializing = false;
 
             mReportedVisible = false;
+            Trace.beginSection("WPMS.Engine.updateSurface");
             updateSurface(false, false, false);
+            Trace.endSection();
         }
 
         /**
@@ -2236,14 +2254,15 @@
         public void reportShown() {
             if (!mShownReported) {
                 mShownReported = true;
+                Trace.beginSection("WPMS.mConnection.engineShown");
                 try {
                     mConnection.engineShown(this);
                     Log.d(TAG, "Wallpaper has updated the surface:"
                             + mWallpaperManager.getWallpaperInfo());
                 } catch (RemoteException e) {
                     Log.w(TAG, "Wallpaper host disappeared", e);
-                    return;
                 }
+                Trace.endSection();
             }
         }
 
@@ -2285,6 +2304,27 @@
             return mEngine == null ? null : SurfaceControl.mirrorSurface(mEngine.mSurfaceControl);
         }
 
+        private void doAttachEngine() {
+            Trace.beginSection("WPMS.onCreateEngine");
+            Engine engine = onCreateEngine();
+            Trace.endSection();
+            mEngine = engine;
+            Trace.beginSection("WPMS.mConnection.attachEngine-" + mDisplayId);
+            try {
+                mConnection.attachEngine(this, mDisplayId);
+            } catch (RemoteException e) {
+                engine.detach();
+                Log.w(TAG, "Wallpaper host disappeared", e);
+                return;
+            } finally {
+                Trace.endSection();
+            }
+            mActiveEngines.add(engine);
+            Trace.beginSection("WPMS.engine.attach");
+            engine.attach(this);
+            Trace.endSection();
+        }
+
         private void doDetachEngine() {
             mActiveEngines.remove(mEngine);
             mEngine.detach();
@@ -2310,21 +2350,15 @@
             }
             switch (message.what) {
                 case DO_ATTACH: {
-                    Engine engine = onCreateEngine();
-                    mEngine = engine;
-                    try {
-                        mConnection.attachEngine(this, mDisplayId);
-                    } catch (RemoteException e) {
-                        engine.detach();
-                        Log.w(TAG, "Wallpaper host disappeared", e);
-                        return;
-                    }
-                    mActiveEngines.add(engine);
-                    engine.attach(this);
+                    Trace.beginSection("WPMS.DO_ATTACH");
+                    doAttachEngine();
+                    Trace.endSection();
                     return;
                 }
                 case DO_DETACH: {
+                    Trace.beginSection("WPMS.DO_DETACH");
                     doDetachEngine();
+                    Trace.endSection();
                     return;
                 }
                 case DO_SET_DESIRED_SIZE: {
@@ -2405,7 +2439,9 @@
                     }
                 } break;
                 case MSG_REPORT_SHOWN: {
+                    Trace.beginSection("WPMS.MSG_REPORT_SHOWN");
                     reportShown();
+                    Trace.endSection();
                 } break;
                 default :
                     Log.w(TAG, "Unknown message type " + message.what);
@@ -2429,8 +2465,10 @@
         public void attach(IWallpaperConnection conn, IBinder windowToken,
                 int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding,
                 int displayId, @SetWallpaperFlags int which) {
+            Trace.beginSection("WPMS.ServiceWrapper.attach");
             mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken,
                     windowType, isPreview, reqWidth, reqHeight, padding, displayId);
+            Trace.endSection();
         }
 
         @Override
@@ -2441,16 +2479,20 @@
 
     @Override
     public void onCreate() {
+        Trace.beginSection("WPMS.onCreate");
         super.onCreate();
+        Trace.endSection();
     }
 
     @Override
     public void onDestroy() {
+        Trace.beginSection("WPMS.onDestroy");
         super.onDestroy();
         for (int i=0; i<mActiveEngines.size(); i++) {
             mActiveEngines.get(i).detach();
         }
         mActiveEngines.clear();
+        Trace.endSection();
     }
 
     /**
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index c94a372..9b519c3 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -1440,6 +1440,8 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
+     * @return {@code true} if the replace command was sent to the associated editor (regardless of
+     *     whether the replacement is success or not), {@code false} otherwise.
      */
     default boolean replaceText(
             @IntRange(from = 0) int start,
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 06d7cdd..c3623a2 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -210,7 +210,7 @@
 
         mTunerSessions[0].setMuted(/* mute= */ false);
 
-        assertWithMessage("Session mute state after setting muted %s", false)
+        assertWithMessage("Session mute state after setting unmuted")
                 .that(mTunerSessions[0].isMuted()).isFalse();
     }
 
@@ -220,7 +220,7 @@
 
         mTunerSessions[0].setMuted(/* mute= */ true);
 
-        assertWithMessage("Session mute state after setting muted %s", true)
+        assertWithMessage("Session mute state after setting muted")
                 .that(mTunerSessions[0].isMuted()).isTrue();
     }
 
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
index 392e140..ad1b872 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
@@ -22,6 +22,7 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
+import android.util.ArrayMap;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -32,14 +33,33 @@
         throw new UnsupportedOperationException("TestUtils class is noninstantiable");
     }
 
+    static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
+        return new RadioManager.ProgramInfo(selector,
+                selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null,
+                /* infoFlags= */ 0, signalQuality,
+                new RadioMetadata.Builder().build(), new ArrayMap<>());
+    }
+
     static RadioManager.ProgramInfo makeProgramInfo(int programType,
             ProgramSelector.Identifier identifier, int signalQuality) {
         // Note: If you set new fields, check if programInfoToHal() needs to be updated as well.
-        return new RadioManager.ProgramInfo(new ProgramSelector(programType, identifier, null,
-                null), null, null, null, 0, signalQuality, new RadioMetadata.Builder().build(),
+        return new RadioManager.ProgramInfo(makeProgramSelector(programType, identifier), null,
+                null, null, 0, signalQuality, new RadioMetadata.Builder().build(),
                 new HashMap<String, String>());
     }
 
+    static ProgramSelector makeFmSelector(long freq) {
+        return makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
+                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+                        freq));
+    }
+
+    static ProgramSelector makeProgramSelector(int programType,
+            ProgramSelector.Identifier identifier) {
+        return new ProgramSelector(programType, identifier, /* secondaryIds= */ null,
+                /* vendorIds= */ null);
+    }
+
     static ProgramInfo programInfoToHal(RadioManager.ProgramInfo info) {
         // Note that because Convert does not by design provide functions for all conversions, this
         // function only copies fields that are set by makeProgramInfo().
@@ -57,10 +77,22 @@
         android.hardware.broadcastradio.V2_0.ProgramSelector halSelector =
                 new android.hardware.broadcastradio.V2_0.ProgramSelector();
         halSelector.primaryId = halId;
-        halSelector.secondaryIds = new ArrayList<>();
+        halSelector.secondaryIds = new ArrayList<ProgramIdentifier>();
         return halSelector;
     }
 
+    static ProgramInfo makeHalProgramInfo(
+            android.hardware.broadcastradio.V2_0.ProgramSelector hwSel, int hwSignalQuality) {
+        ProgramInfo hwInfo = new ProgramInfo();
+        hwInfo.selector = hwSel;
+        hwInfo.logicallyTunedTo = hwSel.primaryId;
+        hwInfo.physicallyTunedTo = hwSel.primaryId;
+        hwInfo.signalQuality = hwSignalQuality;
+        hwInfo.relatedContent = new ArrayList<>();
+        hwInfo.metadata = new ArrayList<>();
+        return hwInfo;
+    }
+
     static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) {
         VendorKeyValue vendorKeyValue = new VendorKeyValue();
         vendorKeyValue.key = vendorKey;
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
new file mode 100644
index 0000000..9b62dc7
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio.hal2;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.V2_0.Constants;
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.broadcastradio.V2_0.ITunerCallback;
+import android.hardware.broadcastradio.V2_0.ITunerSession;
+import android.hardware.broadcastradio.V2_0.IdentifierType;
+import android.hardware.broadcastradio.V2_0.ProgramInfo;
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.util.ArraySet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.verification.VerificationWithTimeout;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for HIDL HAL TunerSession.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class TunerSessionHidlTest {
+
+    private static final VerificationWithTimeout CALLBACK_TIMEOUT =
+            timeout(/* millis= */ 200);
+    private static final int SIGNAL_QUALITY = 1;
+    private static final long AM_FM_FREQUENCY_SPACING = 500;
+    private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100};
+    private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
+            new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
+                    /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100,
+                    /* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false,
+                    /* ea= */ false);
+    private static final RadioManager.BandConfig FM_BAND_CONFIG =
+            new RadioManager.FmBandConfig(FM_BAND_DESCRIPTOR);
+    private static final int UNSUPPORTED_CONFIG_FLAG = 0;
+
+    private final Object mLock = new Object();
+    private RadioModule mRadioModule;
+    private ITunerCallback mHalTunerCallback;
+    private ProgramInfo mHalCurrentInfo;
+    private TunerSession[] mTunerSessions;
+
+    @Mock private IBroadcastRadio mBroadcastRadioMock;
+    @Mock ITunerSession mHalTunerSessionMock;
+    private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks;
+
+    @Before
+    public void setup() throws Exception {
+        mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(
+                /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
+                /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
+                /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
+                /* isCaptureSupported= */ false, /* bands= */ null, /* isBgScanSupported= */ false,
+                new int[] {}, new int[] {},
+                /* dabFrequencyTable= */ null, /* vendorInfo= */ null), mLock);
+
+        doAnswer(invocation -> {
+            mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
+            IBroadcastRadio.openSessionCallback cb = (IBroadcastRadio.openSessionCallback)
+                    invocation.getArguments()[1];
+            cb.onValues(Result.OK, mHalTunerSessionMock);
+            return null;
+        }).when(mBroadcastRadioMock).openSession(any(), any());
+
+        doAnswer(invocation -> {
+            android.hardware.broadcastradio.V2_0.ProgramSelector halSel =
+                    (android.hardware.broadcastradio.V2_0.ProgramSelector)
+                            invocation.getArguments()[0];
+            mHalCurrentInfo = TestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY);
+            if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY) {
+                return Result.NOT_SUPPORTED;
+            }
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).tune(any());
+
+        doAnswer(invocation -> {
+            if ((boolean) invocation.getArguments()[0]) {
+                mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING;
+            } else {
+                mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING;
+            }
+            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).step(anyBoolean());
+
+        doAnswer(invocation -> {
+            if (mHalCurrentInfo == null) {
+                android.hardware.broadcastradio.V2_0.ProgramSelector placeHolderSelector =
+                        TestUtils.makeHalFmSelector(/* freq= */ 97300);
+
+                mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector);
+                return Result.OK;
+            }
+            mHalCurrentInfo.selector.primaryId.value = getSeekFrequency(
+                    mHalCurrentInfo.selector.primaryId.value,
+                    !(boolean) invocation.getArguments()[0]);
+            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).scan(anyBoolean(), anyBoolean());
+
+        when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(new ArrayList<Byte>(0));
+    }
+
+    @Test
+    public void openSession_withMultipleSessions() throws Exception {
+        int numSessions = 3;
+
+        openAidlClients(numSessions);
+
+        for (int index = 0; index < numSessions; index++) {
+            assertWithMessage("Session of index %s close state", index)
+                    .that(mTunerSessions[index].isClosed()).isFalse();
+        }
+    }
+
+    @Test
+    public void setConfiguration() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onConfigurationChanged(FM_BAND_CONFIG);
+    }
+
+    @Test
+    public void getConfiguration() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
+
+        RadioManager.BandConfig config = mTunerSessions[0].getConfiguration();
+
+        assertWithMessage("Session configuration").that(config)
+                .isEqualTo(FM_BAND_CONFIG);
+    }
+
+    @Test
+    public void setMuted_withUnmuted() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].setMuted(/* mute= */ false);
+
+        assertWithMessage("Session mute state after setting unmuted")
+                .that(mTunerSessions[0].isMuted()).isFalse();
+    }
+
+    @Test
+    public void setMuted_withMuted() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].setMuted(/* mute= */ true);
+
+        assertWithMessage("Session mute state after setting muted")
+                .that(mTunerSessions[0].isMuted()).isTrue();
+    }
+
+    @Test
+    public void close_withOneSession() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].close();
+
+        assertWithMessage("Close state of broadcast radio service session")
+                .that(mTunerSessions[0].isClosed()).isTrue();
+    }
+
+    @Test
+    public void close_withOnlyOneSession_withMultipleSessions() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        int closeIdx = 0;
+
+        mTunerSessions[closeIdx].close();
+
+        for (int index = 0; index < numSessions; index++) {
+            if (index == closeIdx) {
+                assertWithMessage(
+                        "Close state of broadcast radio service session of index %s", index)
+                        .that(mTunerSessions[index].isClosed()).isTrue();
+            } else {
+                assertWithMessage(
+                        "Close state of broadcast radio service session of index %s", index)
+                        .that(mTunerSessions[index].isClosed()).isFalse();
+            }
+        }
+    }
+
+    @Test
+    public void close_withOneSession_withError() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int errorCode = RadioTuner.ERROR_SERVER_DIED;
+
+        mTunerSessions[0].close(errorCode);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onError(errorCode);
+        assertWithMessage("Close state of broadcast radio service session")
+                .that(mTunerSessions[0].isClosed()).isTrue();
+    }
+
+    @Test
+    public void closeSessions_withMultipleSessions_withError() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+
+        int errorCode = RadioTuner.ERROR_SERVER_DIED;
+        mRadioModule.closeSessions(errorCode);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT).onError(errorCode);
+            assertWithMessage("Close state of broadcast radio service session of index %s", index)
+                    .that(mTunerSessions[index].isClosed()).isTrue();
+        }
+    }
+
+    @Test
+    public void tune_withOneSession() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        RadioManager.ProgramInfo tuneInfo =
+                TestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
+
+        mTunerSessions[0].tune(initialSel);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onCurrentProgramInfoChanged(tuneInfo);
+    }
+
+    @Test
+    public void tune_withMultipleSessions() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        RadioManager.ProgramInfo tuneInfo =
+                TestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
+
+        mTunerSessions[0].tune(initialSel);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onCurrentProgramInfoChanged(tuneInfo);
+        }
+    }
+
+    @Test
+    public void tune_withUnsupportedSelector_throwsException() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramSelector unsupportedSelector = TestUtils.makeProgramSelector(
+                ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300));
+
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> mTunerSessions[0].tune(unsupportedSelector));
+
+        assertWithMessage("Exception for tuning on unsupported program selector")
+                .that(thrown).hasMessageThat().contains("tune: NOT_SUPPORTED");
+    }
+
+    @Test
+    public void step_withDirectionUp() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[1];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo stepUpInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(initFreq + AM_FM_FREQUENCY_SPACING), SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].step(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(stepUpInfo);
+    }
+
+    @Test
+    public void step_withDirectionDown() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[1];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo stepDownInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(initFreq - AM_FM_FREQUENCY_SPACING),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(stepDownInfo);
+    }
+
+    @Test
+    public void scan_withDirectionUp() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[2];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(scanUpInfo);
+    }
+
+    @Test
+    public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+        int numSessions = 2;
+        openAidlClients(numSessions);
+
+        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onTuneFailed(eq(Result.TIMEOUT), any());
+        }
+    }
+
+    @Test
+    public void scan_withDirectionDown() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[2];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(scanUpInfo);
+    }
+
+    @Test
+    public void cancel() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        mTunerSessions[0].tune(initialSel);
+
+        mTunerSessions[0].cancel();
+
+        verify(mHalTunerSessionMock).cancel();
+    }
+
+    @Test
+    public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int imageId = Constants.INVALID_IMAGE;
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            mTunerSessions[0].getImage(imageId);
+        });
+
+        assertWithMessage("Exception for getting image with invalid ID")
+                .that(thrown).hasMessageThat().contains("Image ID is missing");
+    }
+
+    @Test
+    public void getImage_withValidId() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int imageId = 1;
+
+        Bitmap imageTest = mTunerSessions[0].getImage(imageId);
+
+        assertWithMessage("Null image").that(imageTest).isEqualTo(null);
+    }
+
+    @Test
+    public void startBackgroundScan() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].startBackgroundScan();
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onBackgroundScanComplete();
+    }
+
+    @Test
+    public void stopProgramListUpdates() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
+                /* includeCategories= */ true, /* excludeModifications= */ false);
+        mTunerSessions[0].startProgramListUpdates(aidlFilter);
+
+        mTunerSessions[0].stopProgramListUpdates();
+
+        verify(mHalTunerSessionMock).stopProgramListUpdates();
+    }
+
+    private void openAidlClients(int numClients) throws Exception {
+        mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
+        mTunerSessions = new TunerSession[numClients];
+        for (int index = 0; index < numClients; index++) {
+            mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class);
+            mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]);
+        }
+    }
+
+    private long getSeekFrequency(long currentFrequency, boolean seekDown) {
+        long seekFrequency;
+        if (seekDown) {
+            seekFrequency = AM_FM_FREQUENCY_LIST[AM_FM_FREQUENCY_LIST.length - 1];
+            for (int i = AM_FM_FREQUENCY_LIST.length - 1; i >= 0; i--) {
+                if (AM_FM_FREQUENCY_LIST[i] < currentFrequency) {
+                    seekFrequency = AM_FM_FREQUENCY_LIST[i];
+                    break;
+                }
+            }
+        } else {
+            seekFrequency = AM_FM_FREQUENCY_LIST[0];
+            for (int index = 0; index < AM_FM_FREQUENCY_LIST.length; index++) {
+                if (AM_FM_FREQUENCY_LIST[index] > currentFrequency) {
+                    seekFrequency = AM_FM_FREQUENCY_LIST[index];
+                    break;
+                }
+            }
+        }
+        return seekFrequency;
+    }
+}
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index b292d7d..a0ed026 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -294,10 +294,9 @@
 
         StopActivityItem lifecycleRequest = StopActivityItem.obtain(78 /* configChanges */);
 
-        IApplicationThread appThread = new StubAppThread();
         Binder activityToken = new Binder();
 
-        ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken);
+        ClientTransaction transaction = ClientTransaction.obtain(null, activityToken);
         transaction.addCallback(callback1);
         transaction.addCallback(callback2);
         transaction.setLifecycleStateRequest(lifecycleRequest);
@@ -318,10 +317,9 @@
         ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
                 config());
 
-        IApplicationThread appThread = new StubAppThread();
         Binder activityToken = new Binder();
 
-        ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken);
+        ClientTransaction transaction = ClientTransaction.obtain(null, activityToken);
         transaction.addCallback(callback1);
         transaction.addCallback(callback2);
 
@@ -339,10 +337,9 @@
         // Write to parcel
         StopActivityItem lifecycleRequest = StopActivityItem.obtain(78 /* configChanges */);
 
-        IApplicationThread appThread = new StubAppThread();
         Binder activityToken = new Binder();
 
-        ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken);
+        ClientTransaction transaction = ClientTransaction.obtain(null, activityToken);
         transaction.setLifecycleStateRequest(lifecycleRequest);
 
         writeAndPrepareForReading(transaction);
@@ -400,286 +397,4 @@
             }
         };
     }
-
-    /** Stub implementation of IApplicationThread that can be presented as {@link Binder}. */
-    class StubAppThread extends android.app.IApplicationThread.Stub  {
-
-        @Override
-        public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleReceiver(Intent intent, ActivityInfo activityInfo,
-                CompatibilityInfo compatibilityInfo, int i, String s, Bundle bundle, boolean b,
-                int i1, int i2) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleCreateService(IBinder iBinder, ServiceInfo serviceInfo,
-                CompatibilityInfo compatibilityInfo, int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleStopService(IBinder iBinder) throws RemoteException {
-        }
-
-        @Override
-        public void bindApplication(String s, ApplicationInfo applicationInfo,
-                String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage,
-                ProviderInfoList list, ComponentName componentName, ProfilerInfo profilerInfo,
-                Bundle bundle, IInstrumentationWatcher iInstrumentationWatcher,
-                IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1,
-                boolean b2, boolean b3, Configuration configuration,
-                CompatibilityInfo compatibilityInfo, Map map, Bundle bundle1, String s1,
-                AutofillOptions ao, ContentCaptureOptions co, long[] disableCompatChanges,
-                SharedMemory serializedSystemFontMap,
-                long startRequestedElapsedTime, long startRequestedUptime)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleExit() throws RemoteException {
-        }
-
-        @Override
-        public void scheduleServiceArgs(IBinder iBinder, ParceledListSlice parceledListSlice)
-                throws RemoteException {
-        }
-
-        @Override
-        public void updateTimeZone() throws RemoteException {
-        }
-
-        @Override
-        public void processInBackground() throws RemoteException {
-        }
-
-        @Override
-        public void scheduleBindService(IBinder iBinder, Intent intent, boolean b, int i)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleUnbindService(IBinder iBinder, Intent intent) throws RemoteException {
-        }
-
-        @Override
-        public void dumpService(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder,
-                String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleRegisteredReceiver(IIntentReceiver iIntentReceiver, Intent intent,
-                int i, String s, Bundle bundle, boolean b, boolean b1, int i1, int i2)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleLowMemory() throws RemoteException {
-        }
-
-        @Override
-        public void profilerControl(boolean b, ProfilerInfo profilerInfo, int i)
-                throws RemoteException {
-        }
-
-        @Override
-        public void setSchedulingGroup(int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleCreateBackupAgent(ApplicationInfo applicationInfo,
-                int i, int userId, int operatioType)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleDestroyBackupAgent(ApplicationInfo applicationInfo,
-                int userId) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleOnNewActivityOptions(IBinder iBinder, Bundle bundle)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleSuicide() throws RemoteException {
-        }
-
-        @Override
-        public void dispatchPackageBroadcast(int i, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleCrash(String s, int i, Bundle extras) throws RemoteException {
-        }
-
-        @Override
-        public void dumpActivity(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder,
-                String s, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void clearDnsCache() throws RemoteException {
-        }
-
-        @Override
-        public void updateHttpProxy() throws RemoteException {
-        }
-
-        @Override
-        public void setCoreSettings(Bundle bundle) throws RemoteException {
-        }
-
-        @Override
-        public void updatePackageCompatibilityInfo(String s, CompatibilityInfo compatibilityInfo)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleTrimMemory(int i) throws RemoteException {
-        }
-
-        @Override
-        public void dumpMemInfo(ParcelFileDescriptor parcelFileDescriptor,
-                Debug.MemoryInfo memoryInfo, boolean b, boolean b1, boolean b2, boolean b3,
-                boolean b4, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void dumpMemInfoProto(ParcelFileDescriptor parcelFileDescriptor,
-                Debug.MemoryInfo memoryInfo, boolean b, boolean b1, boolean b2,
-                boolean b3, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void dumpGfxInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings)
-                throws RemoteException {
-        }
-
-        @Override
-        public void dumpCacheInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings)
-                throws RemoteException {
-        }
-
-        @Override
-        public void dumpProvider(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder,
-                String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void dumpDbInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings)
-                throws RemoteException {
-        }
-
-        @Override
-        public void unstableProviderDied(IBinder iBinder) throws RemoteException {
-        }
-
-        @Override
-        public void requestAssistContextExtras(IBinder iBinder, IBinder iBinder1, int i, int i1,
-                int i2) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleTranslucentConversionComplete(IBinder iBinder, boolean b)
-                throws RemoteException {
-        }
-
-        @Override
-        public void setProcessState(int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleInstallProvider(ProviderInfo providerInfo) throws RemoteException {
-        }
-
-        @Override
-        public void updateTimePrefs(int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleEnterAnimationComplete(IBinder iBinder) throws RemoteException {
-        }
-
-        @Override
-        public void notifyCleartextNetwork(byte[] bytes) throws RemoteException {
-        }
-
-        @Override
-        public void startBinderTracking() throws RemoteException {
-        }
-
-        @Override
-        public void stopBinderTrackingAndDump(ParcelFileDescriptor parcelFileDescriptor)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleLocalVoiceInteractionStarted(IBinder iBinder,
-                IVoiceInteractor iVoiceInteractor) throws RemoteException {
-        }
-
-        @Override
-        public void handleTrustStorageUpdate() throws RemoteException {
-        }
-
-        @Override
-        public void attachAgent(String s) throws RemoteException {
-        }
-
-        @Override
-        public void attachStartupAgents(String s) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleApplicationInfoChanged(ApplicationInfo applicationInfo)
-                throws RemoteException {
-        }
-
-        @Override
-        public void setNetworkBlockSeq(long l) throws RemoteException {
-        }
-
-        @Override
-        public void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, String path,
-                ParcelFileDescriptor fd, RemoteCallback finishCallback) {
-        }
-
-        @Override
-        public void dumpResources(ParcelFileDescriptor fd, RemoteCallback finishCallback) {
-        }
-
-        @Override
-        public final void runIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) {
-        }
-
-        @Override
-        public void requestDirectActions(IBinder activityToken, IVoiceInteractor interactor,
-                RemoteCallback cancellationCallback, RemoteCallback resultCallback) {
-        }
-
-        @Override
-        public void performDirectAction(IBinder activityToken, String actionId, Bundle arguments,
-                RemoteCallback cancellationCallback, RemoteCallback resultCallback) {
-        }
-
-        @Override
-        public void notifyContentProviderPublishStatus(ContentProviderHolder holder, String auth,
-                int userId, boolean published) {
-        }
-
-        @Override
-        public void instrumentWithoutRestart(ComponentName instrumentationName,
-                Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher,
-                IUiAutomationConnection instrumentationUiConnection, ApplicationInfo targetInfo) {
-        }
-
-        @Override
-        public void updateUiTranslationState(IBinder activityToken, int state,
-                TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> viewIds,
-                UiTranslationSpec uiTranslationSpec) {
-        }
-    }
 }
diff --git a/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java b/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java
index 98485c0..ee73f00 100644
--- a/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java
+++ b/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java
@@ -29,10 +29,11 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
- * Test class for {@link ConstrainDisplayApisConfig}.
+ * Test for {@link ConstrainDisplayApisConfig}.
  *
  * Build/Install/Run:
  * atest FrameworksCoreTests:ConstrainDisplayApisConfigTest
@@ -72,6 +73,7 @@
         testNeverConstrainDisplayApis("com.android.test", /* version= */ 1, /* expected= */ false);
     }
 
+    @Ignore("b/257375674")
     @Test
     public void neverConstrainDisplayApis_flagsHasSingleEntry_returnsTrueForPackageWithinRange() {
         setNeverConstrainDisplayApisFlag("com.android.test:1:1");
@@ -107,6 +109,7 @@
         testNeverConstrainDisplayApis("com.android.test4", /* version= */ 9, /* expected= */ false);
     }
 
+    @Ignore("b/257375674")
     @Test
     public void neverConstrainDisplayApis_flagHasInvalidEntries_ignoresInvalidEntries() {
         // We add a valid entry before and after the invalid ones to make sure they are applied.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS
new file mode 100644
index 0000000..1e0f9bc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS
@@ -0,0 +1,5 @@
+# WM shell sub-module back navigation owners
+# Bug component: 1152663
+shanh@google.com
+arthurhung@google.com
+wilsonshih@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index a0e176c..ff6f2b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -430,7 +430,8 @@
         }
 
         @Override
-        public @Nullable SplashScreenView get() {
+        @Nullable
+        public SplashScreenView get() {
             synchronized (this) {
                 while (!mIsViewSet) {
                     try {
@@ -691,7 +692,7 @@
         private final TaskSnapshotWindow mTaskSnapshotWindow;
         private SplashScreenView mContentView;
         private boolean mSetSplashScreen;
-        private @StartingWindowType int mSuggestType;
+        @StartingWindowType private int mSuggestType;
         private int mBGColor;
         private final long mCreateTime;
         private int mSystemBarAppearance;
@@ -732,7 +733,7 @@
 
         // Reset the system bar color which set by splash screen, make it align to the app.
         private void clearSystemBarColor() {
-            if (mDecorView == null) {
+            if (mDecorView == null || !mDecorView.isAttachedToWindow()) {
                 return;
             }
             if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 519ec14..56d51bd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -24,7 +24,6 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.fixScale;
-import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
 import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -336,9 +335,12 @@
         boolean isOpening = isOpeningType(info.getType());
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
-            if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) {
+            if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) {
                 // Currently system windows are controlled by WindowState, so don't change their
-                // surfaces. Otherwise their window tokens could be hidden unexpectedly.
+                // surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly.
+                // This includes Wallpaper (always z-ordered at bottom) and IME (associated with
+                // app), because there may not be a transition associated with their visibility
+                // changes, and currently they don't need transition animation.
                 continue;
             }
             final SurfaceControl leash = change.getLeash();
@@ -375,16 +377,7 @@
                     finishT.setAlpha(leash, 1.f);
                 }
             } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
-                // Wallpaper/IME are anomalies: their visibility is tied to other WindowStates.
-                // As a result, we actually can't hide their WindowTokens because there may not be a
-                // transition associated with them becoming visible again. Fortunately, since
-                // wallpapers are always z-ordered to the back, we don't have to worry about it
-                // flickering to the front during reparenting. Similarly, the IME is reparented to
-                // the associated app, so its visibility is coupled. So, an explicit hide is not
-                // needed visually anyways.
-                if ((change.getFlags() & (FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD)) == 0) {
-                    finishT.hide(leash);
-                }
+                finishT.hide(leash);
             }
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS
new file mode 100644
index 0000000..1e0f9bc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS
@@ -0,0 +1,5 @@
+# WM shell sub-module back navigation owners
+# Bug component: 1152663
+shanh@google.com
+arthurhung@google.com
+wilsonshih@google.com
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 6764890..ad6acea 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -438,6 +438,18 @@
                 return "AUDIO_FORMAT_APTX_TWSP";
             case /* AUDIO_FORMAT_LC3             */ 0x2B000000:
                 return "AUDIO_FORMAT_LC3";
+            case /* AUDIO_FORMAT_MPEGH           */ 0x2C000000:
+                return "AUDIO_FORMAT_MPEGH";
+            case /* AUDIO_FORMAT_IEC60958        */ 0x2D000000:
+                return "AUDIO_FORMAT_IEC60958";
+            case /* AUDIO_FORMAT_DTS_UHD         */ 0x2E000000:
+                return "AUDIO_FORMAT_DTS_UHD";
+            case /* AUDIO_FORMAT_DRA             */ 0x2F000000:
+                return "AUDIO_FORMAT_DRA";
+            case /* AUDIO_FORMAT_APTX_ADAPTIVE_QLEA */ 0x30000000:
+                return "AUDIO_FORMAT_APTX_ADAPTIVE_QLEA";
+            case /* AUDIO_FORMAT_APTX_ADAPTIVE_R4   */ 0x31000000:
+                return "AUDIO_FORMAT_APTX_ADAPTIVE_R4";
 
             /* Aliases */
             case /* AUDIO_FORMAT_PCM_16_BIT        */ 0x1:
@@ -510,10 +522,14 @@
                 return "AUDIO_FORMAT_MAT_2_0"; // (MAT | MAT_SUB_2_0)
             case /* AUDIO_FORMAT_MAT_2_1           */ 0x24000003:
                 return "AUDIO_FORMAT_MAT_2_1"; // (MAT | MAT_SUB_2_1)
-            case /* AUDIO_FORMAT_DTS_UHD */           0x2E000000:
-                return "AUDIO_FORMAT_DTS_UHD";
-            case /* AUDIO_FORMAT_DRA */           0x2F000000:
-                return "AUDIO_FORMAT_DRA";
+            case /* AUDIO_FORMAT_MPEGH_SUB_BL_L3   */ 0x2C000013:
+                return "AUDIO_FORMAT_MPEGH_SUB_BL_L3";
+            case /* AUDIO_FORMAT_MPEGH_SUB_BL_L4   */ 0x2C000014:
+                return "AUDIO_FORMAT_MPEGH_SUB_BL_L4";
+            case /* AUDIO_FORMAT_MPEGH_SUB_LC_L3   */ 0x2C000023:
+                return "AUDIO_FORMAT_MPEGH_SUB_LC_L3";
+            case /* AUDIO_FORMAT_MPEGH_SUB_LC_L4   */ 0x2C000024:
+                return "AUDIO_FORMAT_MPEGH_SUB_LC_L4";
             default:
                 return "AUDIO_FORMAT_(" + audioFormat + ")";
         }
@@ -2407,4 +2423,3 @@
      */
     final static int NATIVE_EVENT_ROUTING_CHANGE = 1000;
 }
-
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 25fa34b..1a852c5 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -28,6 +28,7 @@
   <string name="passkeys">passkeys</string>
   <string name="passwords">passwords</string>
   <string name="sign_ins">sign-ins</string>
+  <string name="other_password_manager">Other password manager</string>
   <string name="createOptionInfo_icon_description">CreateOptionInfo credentialType icon</string>
   <!-- Spoken content description of an element which will close the sheet when clicked. -->
   <string name="close_sheet">"Close sheet"</string>
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 2099a23..6e4bfd8 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -28,6 +28,7 @@
 import android.credentials.ui.Entry
 import android.credentials.ui.CreateCredentialProviderData
 import android.credentials.ui.GetCredentialProviderData
+import android.credentials.ui.DisabledProviderData
 import android.credentials.ui.ProviderData
 import android.credentials.ui.RequestInfo
 import android.credentials.ui.BaseDialogResult
@@ -39,7 +40,7 @@
 import com.android.credentialmanager.createflow.ActiveEntry
 import com.android.credentialmanager.createflow.CreatePasskeyUiState
 import com.android.credentialmanager.createflow.CreateScreenState
-import com.android.credentialmanager.createflow.ProviderInfo
+import com.android.credentialmanager.createflow.EnabledProviderInfo
 import com.android.credentialmanager.createflow.RequestDisplayInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
 import com.android.credentialmanager.getflow.GetScreenState
@@ -51,7 +52,8 @@
   intent: Intent,
 ) {
   private val requestInfo: RequestInfo
-  private val providerList: List<ProviderData>
+  private val providerEnabledList: List<ProviderData>
+  private val providerDisabledList: List<DisabledProviderData>
   // TODO: require non-null.
   val resultReceiver: ResultReceiver?
 
@@ -61,16 +63,16 @@
       RequestInfo::class.java
     ) ?: testCreateRequestInfo()
 
-    providerList = when (requestInfo.type) {
+    providerEnabledList = when (requestInfo.type) {
       RequestInfo.TYPE_CREATE ->
         intent.extras?.getParcelableArrayList(
                 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
                 CreateCredentialProviderData::class.java
-        ) ?: testCreateCredentialProviderList()
+        ) ?: testCreateCredentialEnabledProviderList()
       RequestInfo.TYPE_GET ->
         intent.extras?.getParcelableArrayList(
           ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
-          GetCredentialProviderData::class.java
+          DisabledProviderData::class.java
         ) ?: testGetCredentialProviderList()
       else -> {
         // TODO: fail gracefully
@@ -78,6 +80,12 @@
       }
     }
 
+    providerDisabledList =
+      intent.extras?.getParcelableArrayList(
+        ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST,
+        DisabledProviderData::class.java
+      ) ?: testDisabledProviderList()
+
     resultReceiver = intent.getParcelableExtra(
       Constants.EXTRA_RESULT_RECEIVER,
       ResultReceiver::class.java
@@ -103,25 +111,28 @@
   }
 
   fun getCredentialInitialUiState(): GetCredentialUiState {
-    val providerList = GetFlowUtils.toProviderList(
+    val providerEnabledList = GetFlowUtils.toProviderList(
     // TODO: handle runtime cast error
-    providerList as List<GetCredentialProviderData>, context)
+      providerEnabledList as List<GetCredentialProviderData>, context)
     // TODO: covert from real requestInfo
     val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo("tribank")
     return GetCredentialUiState(
-      providerList,
+      providerEnabledList,
       GetScreenState.PRIMARY_SELECTION,
       requestDisplayInfo,
     )
   }
 
   fun createPasskeyInitialUiState(): CreatePasskeyUiState {
-    val providerList = CreateFlowUtils.toProviderList(
+    val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
       // Handle runtime cast error
-      providerList as List<CreateCredentialProviderData>, context)
+      providerEnabledList as List<CreateCredentialProviderData>, context)
+    val providerDisabledList = CreateFlowUtils.toDisabledProviderList(
+      // Handle runtime cast error
+      providerDisabledList as List<DisabledProviderData>, context)
     var hasDefault = false
-    var defaultProvider: ProviderInfo = providerList.first()
-    providerList.forEach{providerInfo -> providerInfo.createOptions =
+    var defaultProvider: EnabledProviderInfo = providerEnabledList.first()
+    providerEnabledList.forEach{providerInfo -> providerInfo.createOptions =
       providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed()
       if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} }
     // TODO: covert from real requestInfo
@@ -131,7 +142,8 @@
       TYPE_PUBLIC_KEY_CREDENTIAL,
       "tribank")
     return CreatePasskeyUiState(
-      providers = providerList,
+      enabledProviders = providerEnabledList,
+      disabledProviders = providerDisabledList,
       if (hasDefault)
       {CreateScreenState.CREATION_OPTION_SELECTION} else {CreateScreenState.PASSKEY_INTRO},
       requestDisplayInfo,
@@ -157,7 +169,7 @@
   }
 
   // TODO: below are prototype functionalities. To be removed for productionization.
-  private fun testCreateCredentialProviderList(): List<CreateCredentialProviderData> {
+  private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> {
     return listOf(
       CreateCredentialProviderData
         .Builder("com.google/com.google.CredentialManagerService")
@@ -185,6 +197,13 @@
     )
   }
 
+  private fun testDisabledProviderList(): List<DisabledProviderData> {
+    return listOf(
+      DisabledProviderData("LastPass"),
+      DisabledProviderData("Xyzinstalledbutdisabled"),
+    )
+  }
+
   private fun testGetCredentialProviderList(): List<GetCredentialProviderData> {
     return listOf(
       GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 5c79564..e4fab07 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -20,6 +20,7 @@
 import android.credentials.ui.Entry
 import android.credentials.ui.GetCredentialProviderData
 import android.credentials.ui.CreateCredentialProviderData
+import android.credentials.ui.DisabledProviderData
 import com.android.credentialmanager.createflow.CreateOptionInfo
 import com.android.credentialmanager.getflow.ActionEntryInfo
 import com.android.credentialmanager.getflow.AuthenticationEntryInfo
@@ -103,12 +104,12 @@
 class CreateFlowUtils {
   companion object {
 
-    fun toProviderList(
+    fun toEnabledProviderList(
       providerDataList: List<CreateCredentialProviderData>,
       context: Context,
-    ): List<com.android.credentialmanager.createflow.ProviderInfo> {
+    ): List<com.android.credentialmanager.createflow.EnabledProviderInfo> {
       return providerDataList.map {
-        com.android.credentialmanager.createflow.ProviderInfo(
+        com.android.credentialmanager.createflow.EnabledProviderInfo(
           // TODO: replace to extract from the service data structure when available
           icon = context.getDrawable(R.drawable.ic_passkey)!!,
           name = it.providerFlattenedComponentName,
@@ -119,6 +120,20 @@
       }
     }
 
+    fun toDisabledProviderList(
+      providerDataList: List<DisabledProviderData>,
+      context: Context,
+    ): List<com.android.credentialmanager.createflow.DisabledProviderInfo> {
+      return providerDataList.map {
+        com.android.credentialmanager.createflow.DisabledProviderInfo(
+          // TODO: replace to extract from the service data structure when available
+          icon = context.getDrawable(R.drawable.ic_passkey)!!,
+          name = it.providerFlattenedComponentName,
+          displayName = it.providerFlattenedComponentName,
+        )
+      }
+    }
+
     private fun toCreationOptionInfoList(
       creationEntries: List<Entry>,
       context: Context,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 21190e7..0c3447f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -18,13 +18,25 @@
 
 import android.graphics.drawable.Drawable
 
-data class ProviderInfo(
+open class ProviderInfo(
   val icon: Drawable,
   val name: String,
   val displayName: String,
+)
+
+class EnabledProviderInfo(
+  icon: Drawable,
+  name: String,
+  displayName: String,
   var createOptions: List<CreateOptionInfo>,
   val isDefault: Boolean,
-)
+) : ProviderInfo(icon, name, displayName)
+
+class DisabledProviderInfo(
+  icon: Drawable,
+  name: String,
+  displayName: String,
+) : ProviderInfo(icon, name, displayName)
 
 open class EntryInfo (
   val entryKey: String,
@@ -55,7 +67,7 @@
  * user selects a different entry on the more option page.
  */
 data class ActiveEntry (
-  val activeProvider: ProviderInfo,
+  val activeProvider: EnabledProviderInfo,
   val activeEntryInfo: EntryInfo,
 )
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 437e7b21..06e437c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -21,6 +21,7 @@
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Add
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Alignment
@@ -59,7 +60,7 @@
           onCancel = viewModel::onCancel,
         )
         CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
-          providerList = uiState.providers,
+          enabledProviderList = uiState.enabledProviders,
           onCancel = viewModel::onCancel,
           onProviderSelected = viewModel::onProviderSelected
         )
@@ -70,14 +71,16 @@
           onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected,
           onConfirm = viewModel::onPrimaryCreateOptionInfoSelected,
           onCancel = viewModel::onCancel,
-          multiProvider = uiState.providers.size > 1,
+          multiProvider = uiState.enabledProviders.size > 1,
           onMoreOptionsSelected = viewModel::onMoreOptionsSelected
         )
         CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
             requestDisplayInfo = uiState.requestDisplayInfo,
-            providerList = uiState.providers,
+            enabledProviderList = uiState.enabledProviders,
+            disabledProviderList = uiState.disabledProviders,
             onBackButtonSelected = viewModel::onBackButtonSelected,
-            onOptionSelected = viewModel::onMoreOptionsRowSelected
+            onOptionSelected = viewModel::onMoreOptionsRowSelected,
+            onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected
           )
         CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
           providerInfo = uiState.activeEntry?.activeProvider!!,
@@ -153,7 +156,7 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun ProviderSelectionCard(
-  providerList: List<ProviderInfo>,
+  enabledProviderList: List<EnabledProviderInfo>,
   onProviderSelected: (String) -> Unit,
   onCancel: () -> Unit
 ) {
@@ -182,7 +185,7 @@
         LazyColumn(
           verticalArrangement = Arrangement.spacedBy(2.dp)
         ) {
-          providerList.forEach {
+          enabledProviderList.forEach {
             item {
               ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected)
             }
@@ -212,9 +215,11 @@
 @Composable
 fun MoreOptionsSelectionCard(
   requestDisplayInfo: RequestDisplayInfo,
-  providerList: List<ProviderInfo>,
+  enabledProviderList: List<EnabledProviderInfo>,
+  disabledProviderList: List<DisabledProviderInfo>,
   onBackButtonSelected: () -> Unit,
-  onOptionSelected: (ActiveEntry) -> Unit
+  onOptionSelected: (ActiveEntry) -> Unit,
+  onDisabledPasswordManagerSelected: () -> Unit,
 ) {
   Card() {
     Column() {
@@ -250,18 +255,24 @@
         LazyColumn(
           verticalArrangement = Arrangement.spacedBy(2.dp)
         ) {
-          providerList.forEach { providerInfo ->
-            providerInfo.createOptions.forEach { createOptionInfo ->
+          enabledProviderList.forEach { enabledProviderInfo ->
+            enabledProviderInfo.createOptions.forEach { createOptionInfo ->
               item {
                 MoreOptionsInfoRow(
-                  providerInfo = providerInfo,
+                  providerInfo = enabledProviderInfo,
                   createOptionInfo = createOptionInfo,
                   onOptionSelected = {
-                    onOptionSelected(ActiveEntry(providerInfo, createOptionInfo))
+                    onOptionSelected(ActiveEntry(enabledProviderInfo, createOptionInfo))
                   })
               }
             }
           }
+          item {
+            MoreOptionsDisabledProvidersRow(
+              disabledProviders = disabledProviderList,
+              onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected,
+            )
+          }
         }
       }
       Divider(
@@ -276,7 +287,7 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun MoreOptionsRowIntroCard(
-  providerInfo: ProviderInfo,
+  providerInfo: EnabledProviderInfo,
   onDefaultOrNotSelected: () -> Unit,
 ) {
   Card() {
@@ -483,7 +494,7 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun MoreOptionsInfoRow(
-  providerInfo: ProviderInfo,
+  providerInfo: EnabledProviderInfo,
   createOptionInfo: CreateOptionInfo,
   onOptionSelected: () -> Unit
 ) {
@@ -546,4 +557,37 @@
           }
         }
     )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreOptionsDisabledProvidersRow(
+  disabledProviders: List<ProviderInfo>,
+  onDisabledPasswordManagerSelected: () -> Unit,
+) {
+  SuggestionChip(
+    modifier = Modifier.fillMaxWidth(),
+    onClick = onDisabledPasswordManagerSelected,
+    icon = {
+      Icon(
+        Icons.Filled.Add,
+        contentDescription = null
+      )
+    },
+    shape = MaterialTheme.shapes.large,
+    label = {
+      Column() {
+        Text(
+          text = stringResource(R.string.other_password_manager),
+          style = MaterialTheme.typography.titleLarge,
+          modifier = Modifier.padding(top = 16.dp)
+        )
+        Text(
+          text = disabledProviders.joinToString(separator = ", "){ it.displayName },
+          style = MaterialTheme.typography.bodyMedium,
+          modifier = Modifier.padding(bottom = 16.dp)
+        )
+      }
+    }
+  )
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index 2e9758a..8b94201 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -28,7 +28,8 @@
 import com.android.credentialmanager.common.ResultState
 
 data class CreatePasskeyUiState(
-  val providers: List<ProviderInfo>,
+  val enabledProviders: List<EnabledProviderInfo>,
+  val disabledProviders: List<DisabledProviderInfo>,
   val currentScreenState: CreateScreenState,
   val requestDisplayInfo: RequestDisplayInfo,
   val activeEntry: ActiveEntry? = null,
@@ -50,15 +51,15 @@
   }
 
   fun onConfirmIntro() {
-    if (uiState.providers.size > 1) {
+    if (uiState.enabledProviders.size > 1) {
       uiState = uiState.copy(
         currentScreenState = CreateScreenState.PROVIDER_SELECTION
       )
-    } else if (uiState.providers.size == 1){
+    } else if (uiState.enabledProviders.size == 1){
       uiState = uiState.copy(
         currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        activeEntry = ActiveEntry(uiState.providers.first(),
-          uiState.providers.first().createOptions.first())
+        activeEntry = ActiveEntry(uiState.enabledProviders.first(),
+          uiState.enabledProviders.first().createOptions.first())
       )
     } else {
       throw java.lang.IllegalStateException("Empty provider list.")
@@ -73,8 +74,8 @@
     )
   }
 
-  fun getProviderInfoByName(providerName: String): ProviderInfo {
-    return uiState.providers.single {
+  fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
+    return uiState.enabledProviders.single {
       it.name.equals(providerName)
     }
   }
@@ -98,6 +99,10 @@
     )
   }
 
+  fun onDisabledPasswordManagerSelected() {
+    // TODO: Complete this function
+  }
+
   fun onCancel() {
     CredentialManagerRepo.getInstance().onCancel()
     dialogResult.value = DialogResult(ResultState.CANCELED)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
new file mode 100644
index 0000000..7db1ca1
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright 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.widget.scaffold
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.animation.core.animateTo
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.TopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.compose.horizontalValues
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun CustomizedTopAppBar(
+    title: @Composable () -> Unit,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+) {
+    SingleRowTopAppBar(
+        title = title,
+        titleTextStyle = MaterialTheme.typography.titleMedium,
+        navigationIcon = navigationIcon,
+        actions = actions,
+        windowInsets = TopAppBarDefaults.windowInsets,
+        colors = topAppBarColors(),
+    )
+}
+
+/**
+ * The customized LargeTopAppBar for Settings.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun CustomizedLargeTopAppBar(
+    title: String,
+    modifier: Modifier = Modifier,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+    scrollBehavior: TopAppBarScrollBehavior? = null,
+) {
+    TwoRowsTopAppBar(
+        title = { Title(title = title, maxLines = 2) },
+        titleTextStyle = MaterialTheme.typography.displaySmall,
+        smallTitleTextStyle = MaterialTheme.typography.titleMedium,
+        titleBottomPadding = LargeTitleBottomPadding,
+        smallTitle = { Title(title = title, maxLines = 1) },
+        modifier = modifier,
+        navigationIcon = navigationIcon,
+        actions = actions,
+        colors = topAppBarColors(),
+        windowInsets = TopAppBarDefaults.windowInsets,
+        maxHeight = 176.dp,
+        pinnedHeight = ContainerHeight,
+        scrollBehavior = scrollBehavior,
+    )
+}
+
+@Composable
+private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
+    Text(
+        text = title,
+        modifier = Modifier
+            .padding(
+                WindowInsets.navigationBars
+                    .asPaddingValues()
+                    .horizontalValues()
+            )
+            .padding(horizontal = SettingsDimension.itemPaddingAround),
+        overflow = TextOverflow.Ellipsis,
+        maxLines = maxLines,
+    )
+}
+
+@Composable
+private fun topAppBarColors() = TopAppBarColors(
+    containerColor = MaterialTheme.colorScheme.background,
+    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
+    navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
+    titleContentColor = MaterialTheme.colorScheme.onSurface,
+    actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+)
+
+/**
+ * Represents the colors used by a top app bar in different states.
+ * This implementation animates the container color according to the top app bar scroll state. It
+ * does not animate the leading, headline, or trailing colors.
+ */
+@Stable
+private class TopAppBarColors(
+    val containerColor: Color,
+    val scrolledContainerColor: Color,
+    val navigationIconContentColor: Color,
+    val titleContentColor: Color,
+    val actionIconContentColor: Color,
+) {
+
+    /**
+     * Represents the container color used for the top app bar.
+     *
+     * A [colorTransitionFraction] provides a percentage value that can be used to generate a color.
+     * Usually, an app bar implementation will pass in a [colorTransitionFraction] read from
+     * the [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction].
+     *
+     * @param colorTransitionFraction a `0.0` to `1.0` value that represents a color transition
+     * percentage
+     */
+    @Composable
+    fun containerColor(colorTransitionFraction: Float): Color {
+        return lerp(
+            containerColor,
+            scrolledContainerColor,
+            FastOutLinearInEasing.transform(colorTransitionFraction)
+        )
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || other !is TopAppBarColors) return false
+
+        if (containerColor != other.containerColor) return false
+        if (scrolledContainerColor != other.scrolledContainerColor) return false
+        if (navigationIconContentColor != other.navigationIconContentColor) return false
+        if (titleContentColor != other.titleContentColor) return false
+        if (actionIconContentColor != other.actionIconContentColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = containerColor.hashCode()
+        result = 31 * result + scrolledContainerColor.hashCode()
+        result = 31 * result + navigationIconContentColor.hashCode()
+        result = 31 * result + titleContentColor.hashCode()
+        result = 31 * result + actionIconContentColor.hashCode()
+
+        return result
+    }
+}
+
+/**
+ * A single-row top app bar that is designed to be called by the small and center aligned top app
+ * bar composables.
+ */
+@Composable
+private fun SingleRowTopAppBar(
+    title: @Composable () -> Unit,
+    titleTextStyle: TextStyle,
+    navigationIcon: @Composable () -> Unit,
+    actions: @Composable (RowScope.() -> Unit),
+    windowInsets: WindowInsets,
+    colors: TopAppBarColors,
+) {
+    // Wrap the given actions in a Row.
+    val actionsRow = @Composable {
+        Row(
+            horizontalArrangement = Arrangement.End,
+            verticalAlignment = Alignment.CenterVertically,
+            content = actions
+        )
+    }
+
+    // Compose a Surface with a TopAppBarLayout content.
+    Surface(color = colors.scrolledContainerColor) {
+        val height = LocalDensity.current.run { ContainerHeight.toPx() }
+        TopAppBarLayout(
+            modifier = Modifier
+                .windowInsetsPadding(windowInsets)
+                // clip after padding so we don't show the title over the inset area
+                .clipToBounds(),
+            heightPx = height,
+            navigationIconContentColor = colors.navigationIconContentColor,
+            titleContentColor = colors.titleContentColor,
+            actionIconContentColor = colors.actionIconContentColor,
+            title = title,
+            titleTextStyle = titleTextStyle,
+            titleAlpha = 1f,
+            titleVerticalArrangement = Arrangement.Center,
+            titleHorizontalArrangement = Arrangement.Start,
+            titleBottomPadding = 0,
+            hideTitleSemantics = false,
+            navigationIcon = navigationIcon,
+            actions = actionsRow,
+        )
+    }
+}
+
+/**
+ * A two-rows top app bar that is designed to be called by the Large and Medium top app bar
+ * composables.
+ *
+ * @throws [IllegalArgumentException] if the given [maxHeight] is equal or smaller than the
+ * [pinnedHeight]
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TwoRowsTopAppBar(
+    modifier: Modifier = Modifier,
+    title: @Composable () -> Unit,
+    titleTextStyle: TextStyle,
+    titleBottomPadding: Dp,
+    smallTitle: @Composable () -> Unit,
+    smallTitleTextStyle: TextStyle,
+    navigationIcon: @Composable () -> Unit,
+    actions: @Composable RowScope.() -> Unit,
+    windowInsets: WindowInsets,
+    colors: TopAppBarColors,
+    maxHeight: Dp,
+    pinnedHeight: Dp,
+    scrollBehavior: TopAppBarScrollBehavior?
+) {
+    if (maxHeight <= pinnedHeight) {
+        throw IllegalArgumentException(
+            "A TwoRowsTopAppBar max height should be greater than its pinned height"
+        )
+    }
+    val pinnedHeightPx: Float
+    val maxHeightPx: Float
+    val titleBottomPaddingPx: Int
+    LocalDensity.current.run {
+        pinnedHeightPx = pinnedHeight.toPx()
+        maxHeightPx = maxHeight.toPx()
+        titleBottomPaddingPx = titleBottomPadding.roundToPx()
+    }
+
+    // Sets the app bar's height offset limit to hide just the bottom title area and keep top title
+    // visible when collapsed.
+    SideEffect {
+        if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx) {
+            scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx
+        }
+    }
+
+    // Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the
+    // bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or
+    // collapse.
+    // This will potentially animate or interpolate a transition between the container color and the
+    // container's scrolled color according to the app bar's scroll state.
+    val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction ?: 0f
+    val appBarContainerColor by rememberUpdatedState(colors.containerColor(colorTransitionFraction))
+
+    // Wrap the given actions in a Row.
+    val actionsRow = @Composable {
+        Row(
+            horizontalArrangement = Arrangement.End,
+            verticalAlignment = Alignment.CenterVertically,
+            content = actions
+        )
+    }
+    val topTitleAlpha = TopTitleAlphaEasing.transform(colorTransitionFraction)
+    val bottomTitleAlpha = 1f - colorTransitionFraction
+    // Hide the top row title semantics when its alpha value goes below 0.5 threshold.
+    // Hide the bottom row title semantics when the top title semantics are active.
+    val hideTopRowSemantics = colorTransitionFraction < 0.5f
+    val hideBottomRowSemantics = !hideTopRowSemantics
+
+    // Set up support for resizing the top app bar when vertically dragging the bar itself.
+    val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
+        Modifier.draggable(
+            orientation = Orientation.Vertical,
+            state = rememberDraggableState { delta ->
+                scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffset + delta
+            },
+            onDragStopped = { velocity ->
+                settleAppBar(
+                    scrollBehavior.state,
+                    velocity,
+                    scrollBehavior.flingAnimationSpec,
+                    scrollBehavior.snapAnimationSpec
+                )
+            }
+        )
+    } else {
+        Modifier
+    }
+
+    Surface(modifier = modifier.then(appBarDragModifier), color = appBarContainerColor) {
+        Column {
+            TopAppBarLayout(
+                modifier = Modifier
+                    .windowInsetsPadding(windowInsets)
+                    // clip after padding so we don't show the title over the inset area
+                    .clipToBounds(),
+                heightPx = pinnedHeightPx,
+                navigationIconContentColor = colors.navigationIconContentColor,
+                titleContentColor = colors.titleContentColor,
+                actionIconContentColor = colors.actionIconContentColor,
+                title = smallTitle,
+                titleTextStyle = smallTitleTextStyle,
+                titleAlpha = topTitleAlpha,
+                titleVerticalArrangement = Arrangement.Center,
+                titleHorizontalArrangement = Arrangement.Start,
+                titleBottomPadding = 0,
+                hideTitleSemantics = hideTopRowSemantics,
+                navigationIcon = navigationIcon,
+                actions = actionsRow,
+            )
+            TopAppBarLayout(
+                modifier = Modifier.clipToBounds(),
+                heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset
+                    ?: 0f),
+                navigationIconContentColor = colors.navigationIconContentColor,
+                titleContentColor = colors.titleContentColor,
+                actionIconContentColor = colors.actionIconContentColor,
+                title = title,
+                titleTextStyle = titleTextStyle,
+                titleAlpha = bottomTitleAlpha,
+                titleVerticalArrangement = Arrangement.Bottom,
+                titleHorizontalArrangement = Arrangement.Start,
+                titleBottomPadding = titleBottomPaddingPx,
+                hideTitleSemantics = hideBottomRowSemantics,
+                navigationIcon = {},
+                actions = {}
+            )
+        }
+    }
+}
+
+/**
+ * The base [Layout] for all top app bars. This function lays out a top app bar navigation icon
+ * (leading icon), a title (header), and action icons (trailing icons). Note that the navigation and
+ * the actions are optional.
+ *
+ * @param heightPx the total height this layout is capped to
+ * @param navigationIconContentColor the content color that will be applied via a
+ * [LocalContentColor] when composing the navigation icon
+ * @param titleContentColor the color that will be applied via a [LocalContentColor] when composing
+ * the title
+ * @param actionIconContentColor the content color that will be applied via a [LocalContentColor]
+ * when composing the action icons
+ * @param title the top app bar title (header)
+ * @param titleTextStyle the title's text style
+ * @param modifier a [Modifier]
+ * @param titleAlpha the title's alpha
+ * @param titleVerticalArrangement the title's vertical arrangement
+ * @param titleHorizontalArrangement the title's horizontal arrangement
+ * @param titleBottomPadding the title's bottom padding
+ * @param hideTitleSemantics hides the title node from the semantic tree. Apply this
+ * boolean when this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics
+ * from accessibility services. This is needed to avoid having multiple titles visible to
+ * accessibility services at the same time, when animating between collapsed / expanded states.
+ * @param navigationIcon a navigation icon [Composable]
+ * @param actions actions [Composable]
+ */
+@Composable
+private fun TopAppBarLayout(
+    modifier: Modifier,
+    heightPx: Float,
+    navigationIconContentColor: Color,
+    titleContentColor: Color,
+    actionIconContentColor: Color,
+    title: @Composable () -> Unit,
+    titleTextStyle: TextStyle,
+    titleAlpha: Float,
+    titleVerticalArrangement: Arrangement.Vertical,
+    titleHorizontalArrangement: Arrangement.Horizontal,
+    titleBottomPadding: Int,
+    hideTitleSemantics: Boolean,
+    navigationIcon: @Composable () -> Unit,
+    actions: @Composable () -> Unit,
+) {
+    Layout(
+        {
+            Box(
+                Modifier
+                    .layoutId("navigationIcon")
+                    .padding(start = TopAppBarHorizontalPadding)
+            ) {
+                CompositionLocalProvider(
+                    LocalContentColor provides navigationIconContentColor,
+                    content = navigationIcon
+                )
+            }
+            Box(
+                Modifier
+                    .layoutId("title")
+                    .padding(horizontal = TopAppBarHorizontalPadding)
+                    .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
+                    .graphicsLayer(alpha = titleAlpha)
+            ) {
+                ProvideTextStyle(value = titleTextStyle) {
+                    CompositionLocalProvider(
+                        LocalContentColor provides titleContentColor,
+                        content = title
+                    )
+                }
+            }
+            Box(
+                Modifier
+                    .layoutId("actionIcons")
+                    .padding(end = TopAppBarHorizontalPadding)
+            ) {
+                CompositionLocalProvider(
+                    LocalContentColor provides actionIconContentColor,
+                    content = actions
+                )
+            }
+        },
+        modifier = modifier
+    ) { measurables, constraints ->
+        val navigationIconPlaceable =
+            measurables.first { it.layoutId == "navigationIcon" }
+                .measure(constraints.copy(minWidth = 0))
+        val actionIconsPlaceable =
+            measurables.first { it.layoutId == "actionIcons" }
+                .measure(constraints.copy(minWidth = 0))
+
+        val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {
+            constraints.maxWidth
+        } else {
+            (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
+                .coerceAtLeast(0)
+        }
+        val titlePlaceable =
+            measurables.first { it.layoutId == "title" }
+                .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
+
+        // Locate the title's baseline.
+        val titleBaseline =
+            if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) {
+                titlePlaceable[LastBaseline]
+            } else {
+                0
+            }
+
+        val layoutHeight = heightPx.roundToInt()
+
+        layout(constraints.maxWidth, layoutHeight) {
+            // Navigation icon
+            navigationIconPlaceable.placeRelative(
+                x = 0,
+                y = (layoutHeight - navigationIconPlaceable.height) / 2
+            )
+
+            // Title
+            titlePlaceable.placeRelative(
+                x = when (titleHorizontalArrangement) {
+                    Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
+                    Arrangement.End ->
+                        constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
+                    // Arrangement.Start.
+                    // A TopAppBarTitleInset will make sure the title is offset in case the
+                    // navigation icon is missing.
+                    else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)
+                },
+                y = when (titleVerticalArrangement) {
+                    Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
+                    // Apply bottom padding from the title's baseline only when the Arrangement is
+                    // "Bottom".
+                    Arrangement.Bottom ->
+                        if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
+                        else layoutHeight - titlePlaceable.height - max(
+                            0,
+                            titleBottomPadding - titlePlaceable.height + titleBaseline
+                        )
+                    // Arrangement.Top
+                    else -> 0
+                }
+            )
+
+            // Action icons
+            actionIconsPlaceable.placeRelative(
+                x = constraints.maxWidth - actionIconsPlaceable.width,
+                y = (layoutHeight - actionIconsPlaceable.height) / 2
+            )
+        }
+    }
+}
+
+
+/**
+ * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
+ * after the fling settles.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+private suspend fun settleAppBar(
+    state: TopAppBarState,
+    velocity: Float,
+    flingAnimationSpec: DecayAnimationSpec<Float>?,
+    snapAnimationSpec: AnimationSpec<Float>?
+): Velocity {
+    // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+    // and just return Zero Velocity.
+    // Note that we don't check for 0f due to float precision with the collapsedFraction
+    // calculation.
+    if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+        return Velocity.Zero
+    }
+    var remainingVelocity = velocity
+    // In case there is an initial velocity that was left after a previous user fling, animate to
+    // continue the motion to expand or collapse the app bar.
+    if (flingAnimationSpec != null && abs(velocity) > 1f) {
+        var lastValue = 0f
+        AnimationState(
+            initialValue = 0f,
+            initialVelocity = velocity,
+        )
+            .animateDecay(flingAnimationSpec) {
+                val delta = value - lastValue
+                val initialHeightOffset = state.heightOffset
+                state.heightOffset = initialHeightOffset + delta
+                val consumed = abs(initialHeightOffset - state.heightOffset)
+                lastValue = value
+                remainingVelocity = this.velocity
+                // avoid rounding errors and stop if anything is unconsumed
+                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+            }
+    }
+    // Snap if animation specs were provided.
+    if (snapAnimationSpec != null) {
+        if (state.heightOffset < 0 &&
+            state.heightOffset > state.heightOffsetLimit
+        ) {
+            AnimationState(initialValue = state.heightOffset).animateTo(
+                if (state.collapsedFraction < 0.5f) {
+                    0f
+                } else {
+                    state.heightOffsetLimit
+                },
+                animationSpec = snapAnimationSpec
+            ) { state.heightOffset = value }
+        }
+    }
+
+    return Velocity(0f, remainingVelocity)
+}
+
+// An easing function used to compute the alpha value that is applied to the top title part of a
+// Medium or Large app bar.
+private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
+
+private val ContainerHeight = 56.dp
+private val LargeTitleBottomPadding = 28.dp
+private val TopAppBarHorizontalPadding = 4.dp
+
+// A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the
+// navigation icon is missing.
+private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
index efc623a..b4852e4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -24,17 +24,14 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextField
 import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.runtime.Composable
@@ -136,7 +133,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SearchTopAppBar(
     query: TextFieldValue,
@@ -144,20 +140,16 @@
     onClose: () -> Unit,
     actions: @Composable RowScope.() -> Unit = {},
 ) {
-    Surface(color = SettingsTheme.colorScheme.surfaceHeader) {
-        TopAppBar(
-            title = { SearchBox(query, onQueryChange) },
-            modifier = Modifier.statusBarsPadding(),
-            navigationIcon = { CollapseAction(onClose) },
-            actions = {
-                if (query.text.isNotEmpty()) {
-                    ClearAction { onQueryChange(TextFieldValue()) }
-                }
-                actions()
-            },
-            colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color.Transparent),
-        )
-    }
+    CustomizedTopAppBar(
+        title = { SearchBox(query, onQueryChange) },
+        navigationIcon = { CollapseAction(onClose) },
+        actions = {
+            if (query.text.isNotEmpty()) {
+                ClearAction { onQueryChange(TextFieldValue()) }
+            }
+            actions()
+        },
+    )
     BackHandler { onClose() }
 }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
index f7cb035..3311792 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
@@ -17,24 +17,9 @@
 package com.android.settingslib.spa.widget.scaffold
 
 import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.LargeTopAppBar
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextOverflow
-import com.android.settingslib.spa.framework.compose.horizontalValues
-import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.theme.rememberSettingsTypography
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -43,20 +28,12 @@
     scrollBehavior: TopAppBarScrollBehavior,
     actions: @Composable RowScope.() -> Unit,
 ) {
-    val colorScheme = MaterialTheme.colorScheme
-    // TODO: Remove MaterialTheme() after top app bar color fixed in AndroidX.
-    MaterialTheme(
-        colorScheme = remember { colorScheme.copy(surface = colorScheme.background) },
-        typography = rememberSettingsTypography(),
-    ) {
-        LargeTopAppBar(
-            title = { Title(title) },
-            navigationIcon = { NavigateBack() },
-            actions = actions,
-            colors = largeTopAppBarColors(),
-            scrollBehavior = scrollBehavior,
-        )
-    }
+    CustomizedLargeTopAppBar(
+        title = title,
+        navigationIcon = { NavigateBack() },
+        actions = actions,
+        scrollBehavior = scrollBehavior,
+    )
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
@@ -65,22 +42,3 @@
         heightOffset = heightOffsetLimit
     }
 }
-
-@Composable
-private fun Title(title: String) {
-    Text(
-        text = title,
-        modifier = Modifier
-            .padding(WindowInsets.navigationBars.asPaddingValues().horizontalValues())
-            .padding(SettingsDimension.itemPaddingAround),
-        overflow = TextOverflow.Ellipsis,
-        maxLines = 1,
-    )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun largeTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
-    containerColor = MaterialTheme.colorScheme.background,
-    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
-)
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 6069d91..71ad886 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -116,7 +116,6 @@
         "androidx.exifinterface_exifinterface",
         "androidx.test.ext.junit",
         "com.google.android.material_material",
-        "kotlin-reflect",
         "kotlinx_coroutines_android",
         "kotlinx_coroutines",
         "iconloader_base",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 11237dc..47771aa 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -195,9 +195,6 @@
     <permission android:name="com.android.systemui.permission.FLAGS"
                 android:protectionLevel="signature" />
 
-    <permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
-        android:protectionLevel="signature|privileged" />
-
     <!-- Adding Quick Settings tiles -->
     <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
 
@@ -996,12 +993,5 @@
                   android:excludeFromRecents="true"
                   android:exported="false">
         </activity>
-
-        <provider
-            android:authorities="com.android.systemui.keyguard.quickaffordance"
-            android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
-            android:exported="true"
-            android:permission="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
-            />
     </application>
 </manifest>
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/flags/Flag.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
index 933e586..196f7f0 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
@@ -22,9 +22,19 @@
 import android.os.Parcel
 import android.os.Parcelable
 
+/**
+ * Base interface for flags that can change value on a running device.
+ * @property id unique id to help identify this flag. Must be unique. This will be removed soon.
+ * @property teamfood Set to true to include this flag as part of the teamfood flag. This will
+ *                    be removed soon.
+ * @property name Used for server-side flagging where appropriate. Also used for display. No spaces.
+ * @property namespace The server-side namespace that this flag lives under.
+ */
 interface Flag<T> {
     val id: Int
     val teamfood: Boolean
+    val name: String
+    val namespace: String
 }
 
 interface ParcelableFlag<T> : Flag<T>, Parcelable {
@@ -38,13 +48,10 @@
 }
 
 interface DeviceConfigFlag<T> : Flag<T> {
-    val name: String
-    val namespace: String
     val default: T
 }
 
 interface SysPropFlag<T> : Flag<T> {
-    val name: String
     val default: T
 }
 
@@ -56,6 +63,8 @@
 // Consider using the "parcelize" kotlin library.
 abstract class BooleanFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Boolean = false,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -71,6 +80,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readBoolean(),
         teamfood = parcel.readBoolean(),
         overridden = parcel.readBoolean()
@@ -78,6 +89,8 @@
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeBoolean(default)
         parcel.writeBoolean(teamfood)
         parcel.writeBoolean(overridden)
@@ -91,20 +104,24 @@
  */
 data class UnreleasedFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
-) : BooleanFlag(id, false, teamfood, overridden)
+) : BooleanFlag(id, name, namespace, false, teamfood, overridden)
 
 /**
- * A Flag that is is true by default.
+ * A Flag that is true by default.
  *
  * It can be changed or overridden in any build, meaning it can be turned off if needed.
  */
 data class ReleasedFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
-) : BooleanFlag(id, true, teamfood, overridden)
+) : BooleanFlag(id, name, namespace, true, teamfood, overridden)
 
 /**
  * A Flag that reads its default values from a resource overlay instead of code.
@@ -113,6 +130,8 @@
  */
 data class ResourceBooleanFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     @BoolRes override val resourceId: Int,
     override val teamfood: Boolean = false
 ) : ResourceFlag<Boolean>
@@ -142,7 +161,8 @@
 data class SysPropBooleanFlag constructor(
     override val id: Int,
     override val name: String,
-    override val default: Boolean = false
+    override val namespace: String,
+    override val default: Boolean = false,
 ) : SysPropFlag<Boolean> {
     // TODO(b/223379190): Teamfood not supported for sysprop flags yet.
     override val teamfood: Boolean = false
@@ -150,6 +170,8 @@
 
 data class StringFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: String = "",
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -164,23 +186,31 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readString() ?: ""
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeString(default)
     }
 }
 
 data class ResourceStringFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     @StringRes override val resourceId: Int,
     override val teamfood: Boolean = false
 ) : ResourceFlag<String>
 
 data class IntFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Int = 0,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -196,17 +226,23 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readInt()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeInt(default)
     }
 }
 
 data class ResourceIntFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     @IntegerRes override val resourceId: Int,
     override val teamfood: Boolean = false
 ) : ResourceFlag<Int>
@@ -215,6 +251,8 @@
     override val id: Int,
     override val default: Long = 0,
     override val teamfood: Boolean = false,
+    override val name: String,
+    override val namespace: String,
     override val overridden: Boolean = false
 ) : ParcelableFlag<Long> {
 
@@ -228,17 +266,23 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readLong()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeLong(default)
     }
 }
 
 data class FloatFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Float = 0f,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -254,23 +298,31 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readFloat()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeFloat(default)
     }
 }
 
 data class ResourceFloatFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val resourceId: Int,
-    override val teamfood: Boolean = false
+    override val teamfood: Boolean = false,
 ) : ResourceFlag<Int>
 
 data class DoubleFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Double = 0.0,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -286,11 +338,15 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readDouble()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeDouble(default)
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt
deleted file mode 100644
index 8612b3a..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.shared.keyguard.data.content
-
-import android.annotation.SuppressLint
-import android.content.ContentValues
-import android.content.Context
-import android.database.ContentObserver
-import android.graphics.drawable.Drawable
-import android.net.Uri
-import android.os.UserHandle
-import androidx.annotation.DrawableRes
-import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.withContext
-
-/** Collection of utility functions for using a content provider implementing the [Contract]. */
-object KeyguardQuickAffordanceProviderClient {
-
-    /**
-     * Selects an affordance with the given ID for a slot on the lock screen with the given ID.
-     *
-     * Note that the maximum number of selected affordances on this slot is automatically enforced.
-     * Selecting a slot that is already full (e.g. already has a number of selected affordances at
-     * its maximum capacity) will automatically remove the oldest selected affordance before adding
-     * the one passed in this call. Additionally, selecting an affordance that's already one of the
-     * selected affordances on the slot will move the selected affordance to the newest location in
-     * the slot.
-     */
-    suspend fun insertSelection(
-        context: Context,
-        slotId: String,
-        affordanceId: String,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ) {
-        withContext(dispatcher) {
-            context.contentResolver.insert(
-                Contract.SelectionTable.URI,
-                ContentValues().apply {
-                    put(Contract.SelectionTable.Columns.SLOT_ID, slotId)
-                    put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId)
-                }
-            )
-        }
-    }
-
-    /** Returns all available slots supported by the device. */
-    suspend fun querySlots(
-        context: Context,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): List<Slot> {
-        return withContext(dispatcher) {
-            context.contentResolver
-                .query(
-                    Contract.SlotTable.URI,
-                    null,
-                    null,
-                    null,
-                    null,
-                )
-                ?.use { cursor ->
-                    buildList {
-                        val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID)
-                        val capacityColumnIndex =
-                            cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY)
-                        if (idColumnIndex == -1 || capacityColumnIndex == -1) {
-                            return@buildList
-                        }
-
-                        while (cursor.moveToNext()) {
-                            add(
-                                Slot(
-                                    id = cursor.getString(idColumnIndex),
-                                    capacity = cursor.getInt(capacityColumnIndex),
-                                )
-                            )
-                        }
-                    }
-                }
-        }
-            ?: emptyList()
-    }
-
-    /**
-     * Returns [Flow] for observing the collection of slots.
-     *
-     * @see [querySlots]
-     */
-    fun observeSlots(
-        context: Context,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): Flow<List<Slot>> {
-        return observeUri(
-                context,
-                Contract.SlotTable.URI,
-            )
-            .map { querySlots(context, dispatcher) }
-    }
-
-    /**
-     * Returns all available affordances supported by the device, regardless of current slot
-     * placement.
-     */
-    suspend fun queryAffordances(
-        context: Context,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): List<Affordance> {
-        return withContext(dispatcher) {
-            context.contentResolver
-                .query(
-                    Contract.AffordanceTable.URI,
-                    null,
-                    null,
-                    null,
-                    null,
-                )
-                ?.use { cursor ->
-                    buildList {
-                        val idColumnIndex =
-                            cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID)
-                        val nameColumnIndex =
-                            cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME)
-                        val iconColumnIndex =
-                            cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON)
-                        if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) {
-                            return@buildList
-                        }
-
-                        while (cursor.moveToNext()) {
-                            add(
-                                Affordance(
-                                    id = cursor.getString(idColumnIndex),
-                                    name = cursor.getString(nameColumnIndex),
-                                    iconResourceId = cursor.getInt(iconColumnIndex),
-                                )
-                            )
-                        }
-                    }
-                }
-        }
-            ?: emptyList()
-    }
-
-    /**
-     * Returns [Flow] for observing the collection of affordances.
-     *
-     * @see [queryAffordances]
-     */
-    fun observeAffordances(
-        context: Context,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): Flow<List<Affordance>> {
-        return observeUri(
-                context,
-                Contract.AffordanceTable.URI,
-            )
-            .map { queryAffordances(context, dispatcher) }
-    }
-
-    /** Returns the current slot-affordance selections. */
-    suspend fun querySelections(
-        context: Context,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): List<Selection> {
-        return withContext(dispatcher) {
-            context.contentResolver
-                .query(
-                    Contract.SelectionTable.URI,
-                    null,
-                    null,
-                    null,
-                    null,
-                )
-                ?.use { cursor ->
-                    buildList {
-                        val slotIdColumnIndex =
-                            cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID)
-                        val affordanceIdColumnIndex =
-                            cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID)
-                        if (slotIdColumnIndex == -1 || affordanceIdColumnIndex == -1) {
-                            return@buildList
-                        }
-
-                        while (cursor.moveToNext()) {
-                            add(
-                                Selection(
-                                    slotId = cursor.getString(slotIdColumnIndex),
-                                    affordanceId = cursor.getString(affordanceIdColumnIndex),
-                                )
-                            )
-                        }
-                    }
-                }
-        }
-            ?: emptyList()
-    }
-
-    /**
-     * Returns [Flow] for observing the collection of selections.
-     *
-     * @see [querySelections]
-     */
-    fun observeSelections(
-        context: Context,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): Flow<List<Selection>> {
-        return observeUri(
-                context,
-                Contract.SelectionTable.URI,
-            )
-            .map { querySelections(context, dispatcher) }
-    }
-
-    /** Unselects an affordance with the given ID from the slot with the given ID. */
-    suspend fun deleteSelection(
-        context: Context,
-        slotId: String,
-        affordanceId: String,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ) {
-        withContext(dispatcher) {
-            context.contentResolver.delete(
-                Contract.SelectionTable.URI,
-                "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" +
-                    " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?",
-                arrayOf(
-                    slotId,
-                    affordanceId,
-                ),
-            )
-        }
-    }
-
-    /** Unselects all affordances from the slot with the given ID. */
-    suspend fun deleteAllSelections(
-        context: Context,
-        slotId: String,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ) {
-        withContext(dispatcher) {
-            context.contentResolver.delete(
-                Contract.SelectionTable.URI,
-                "${Contract.SelectionTable.Columns.SLOT_ID}",
-                arrayOf(
-                    slotId,
-                ),
-            )
-        }
-    }
-
-    private fun observeUri(
-        context: Context,
-        uri: Uri,
-    ): Flow<Unit> {
-        return callbackFlow {
-                val observer =
-                    object : ContentObserver(null) {
-                        override fun onChange(selfChange: Boolean) {
-                            trySend(Unit)
-                        }
-                    }
-
-                context.contentResolver.registerContentObserver(
-                    uri,
-                    /* notifyForDescendants= */ true,
-                    observer,
-                    UserHandle.USER_CURRENT,
-                )
-
-                awaitClose { context.contentResolver.unregisterContentObserver(observer) }
-            }
-            .onStart { emit(Unit) }
-    }
-
-    @SuppressLint("UseCompatLoadingForDrawables")
-    suspend fun getAffordanceIcon(
-        context: Context,
-        @DrawableRes iconResourceId: Int,
-        dispatcher: CoroutineDispatcher = Dispatchers.IO,
-    ): Drawable {
-        return withContext(dispatcher) {
-            context.packageManager
-                .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
-                .getDrawable(iconResourceId)
-        }
-    }
-
-    data class Slot(
-        val id: String,
-        val capacity: Int,
-    )
-
-    data class Affordance(
-        val id: String,
-        val name: String,
-        val iconResourceId: Int,
-    )
-
-    data class Selection(
-        val slotId: String,
-        val affordanceId: String,
-    )
-
-    private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
deleted file mode 100644
index c2658a9..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.shared.keyguard.data.content
-
-import android.content.ContentResolver
-import android.net.Uri
-
-/** Contract definitions for querying content about keyguard quick affordances. */
-object KeyguardQuickAffordanceProviderContract {
-
-    const val AUTHORITY = "com.android.systemui.keyguard.quickaffordance"
-    const val PERMISSION = "android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
-
-    private val BASE_URI: Uri =
-        Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
-
-    /**
-     * Table for slots.
-     *
-     * Slots are positions where affordances can be placed on the lock screen. Affordances that are
-     * placed on slots are said to be "selected". The system supports the idea of multiple
-     * affordances per slot, though the implementation may limit the number of affordances on each
-     * slot.
-     *
-     * Supported operations:
-     * - Query - to know which slots are available, query the [SlotTable.URI] [Uri]. The result set
-     * will contain rows with the [SlotTable.Columns] columns.
-     */
-    object SlotTable {
-        const val TABLE_NAME = "slots"
-        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
-
-        object Columns {
-            /** String. Unique ID for this slot. */
-            const val ID = "id"
-            /** Integer. The maximum number of affordances that can be placed in the slot. */
-            const val CAPACITY = "capacity"
-        }
-    }
-
-    /**
-     * Table for affordances.
-     *
-     * Affordances are actions/buttons that the user can execute. They are placed on slots on the
-     * lock screen.
-     *
-     * Supported operations:
-     * - Query - to know about all the affordances that are available on the device, regardless of
-     * which ones are currently selected, query the [AffordanceTable.URI] [Uri]. The result set will
-     * contain rows, each with the columns specified in [AffordanceTable.Columns].
-     */
-    object AffordanceTable {
-        const val TABLE_NAME = "affordances"
-        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
-
-        object Columns {
-            /** String. Unique ID for this affordance. */
-            const val ID = "id"
-            /** String. User-visible name for this affordance. */
-            const val NAME = "name"
-            /**
-             * Integer. Resource ID for the drawable to load for this affordance. This is a resource
-             * ID from the system UI package.
-             */
-            const val ICON = "icon"
-        }
-    }
-
-    /**
-     * Table for selections.
-     *
-     * Selections are pairs of slot and affordance IDs.
-     *
-     * Supported operations:
-     * - Insert - to insert an affordance and place it in a slot, insert values for the columns into
-     * the [SelectionTable.URI] [Uri]. The maximum capacity rule is enforced by the system.
-     * Selecting a new affordance for a slot that is already full will automatically remove the
-     * oldest affordance from the slot.
-     * - Query - to know which affordances are set on which slots, query the [SelectionTable.URI]
-     * [Uri]. The result set will contain rows, each of which with the columns from
-     * [SelectionTable.Columns].
-     * - Delete - to unselect an affordance, removing it from a slot, delete from the
-     * [SelectionTable.URI] [Uri], passing in values for each column.
-     */
-    object SelectionTable {
-        const val TABLE_NAME = "selections"
-        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
-
-        object Columns {
-            /** String. Unique ID for the slot. */
-            const val SLOT_ID = "slot_id"
-            /** String. Unique ID for the selected affordance. */
-            const val AFFORDANCE_ID = "affordance_id"
-        }
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 647dd47..0890465 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -20,7 +20,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
 
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskDescription;
@@ -255,7 +255,8 @@
         // Also consider undefined activity type to include tasks in overview right after rebooting
         // the device.
         final boolean isDockable = taskInfo.supportsMultiWindow
-                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode())
+                && ArrayUtils.contains(
+                        CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, taskInfo.getWindowingMode())
                 && (taskInfo.getActivityType() == ACTIVITY_TYPE_UNDEFINED
                 || ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()));
         return new Task(taskKey,
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
new file mode 100644
index 0000000..74519c2
--- /dev/null
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.flags
+
+import android.annotation.BoolRes
+
+object FlagsFactory {
+    private val flagMap = mutableMapOf<String, Flag<*>>()
+
+    val knownFlags: Map<String, Flag<*>>
+        get() = flagMap
+
+    fun unreleasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): UnreleasedFlag {
+        val flag = UnreleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    fun releasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ReleasedFlag {
+        val flag = ReleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    fun resourceBooleanFlag(
+        id: Int,
+        @BoolRes resourceId: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ResourceBooleanFlag {
+        val flag =
+            ResourceBooleanFlag(
+                id = id,
+                name = name,
+                namespace = namespace,
+                resourceId = resourceId,
+                teamfood = teamfood
+            )
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    fun sysPropBooleanFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        default: Boolean = false
+    ): SysPropBooleanFlag {
+        val flag =
+            SysPropBooleanFlag(id = id, name = name, namespace = "systemui", default = default)
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    private fun checkForDupesAndAdd(flag: Flag<*>) {
+        if (flagMap.containsKey(flag.name)) {
+            throw IllegalArgumentException("Name {flag.name} is already registered")
+        }
+        flagMap.forEach {
+            if (it.value.id == flag.id) {
+                throw IllegalArgumentException("Name {flag.id} is already registered")
+            }
+        }
+        flagMap[flag.name] = flag
+    }
+}
diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt
new file mode 100644
index 0000000..89c0786
--- /dev/null
+++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.flags
+
+import android.annotation.BoolRes
+
+object FlagsFactory {
+    private val flagMap = mutableMapOf<String, Flag<*>>()
+
+    val knownFlags: Map<String, Flag<*>>
+        get() = flagMap
+
+    fun unreleasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): UnreleasedFlag {
+        // Unreleased flags are always false in this build.
+        val flag = UnreleasedFlag(id = id, name = "", namespace = "", teamfood = false)
+        return flag
+    }
+
+    fun releasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ReleasedFlag {
+        val flag = ReleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
+        flagMap[name] = flag
+        return flag
+    }
+
+    fun resourceBooleanFlag(
+        id: Int,
+        @BoolRes resourceId: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ResourceBooleanFlag {
+        val flag =
+            ResourceBooleanFlag(
+                id = id,
+                name = name,
+                namespace = namespace,
+                resourceId = resourceId,
+                teamfood = teamfood
+            )
+        flagMap[name] = flag
+        return flag
+    }
+
+    fun sysPropBooleanFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        default: Boolean = false
+    ): SysPropBooleanFlag {
+        val flag =
+            SysPropBooleanFlag(id = id, name = name, namespace = namespace, default = default)
+        flagMap[name] = flag
+        return flag
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index f74c721..815ac68 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -141,8 +141,7 @@
     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
 
     private final @Background DelayableExecutor mBackgroundExecutor;
-    private int mOrientation;
-    private boolean mSkipFirstLostFocus = false;
+    private boolean mIsOrientationChanged = false;
 
     // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
     @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
@@ -491,6 +490,7 @@
     @Override
     public void onOrientationChanged() {
         maybeUpdatePositionForUdfps(true /* invalidate */);
+        mIsOrientationChanged = true;
     }
 
     @Override
@@ -499,8 +499,8 @@
         if (!hasWindowFocus) {
             //it's a workaround to avoid closing BP incorrectly
             //BP gets a onWindowFocusChanged(false) and then gets a onWindowFocusChanged(true)
-            if (mSkipFirstLostFocus) {
-                mSkipFirstLostFocus = false;
+            if (mIsOrientationChanged) {
+                mIsOrientationChanged = false;
                 return;
             }
             Log.v(TAG, "Lost window focus, dismissing the dialog");
@@ -512,9 +512,6 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
 
-        //save the first orientation
-        mOrientation = getResources().getConfiguration().orientation;
-
         mWakefulnessLifecycle.addObserver(this);
 
         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
@@ -670,7 +667,7 @@
         }
 
         if (savedState != null) {
-            mSkipFirstLostFocus = savedState.getBoolean(
+            mIsOrientationChanged = savedState.getBoolean(
                     AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED);
         }
 
@@ -764,9 +761,7 @@
                 mBiometricView != null && mCredentialView == null);
         outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
 
-        if (mOrientation != getResources().getConfiguration().orientation) {
-            outState.putBoolean(AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED, true);
-        }
+        outState.putBoolean(AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED, mIsOrientationChanged);
 
         if (mBiometricView != null) {
             mBiometricView.onSaveState(outState);
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index 588ef5c..4dfcd63 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -16,16 +16,120 @@
 
 package com.android.systemui.controls
 
+import android.Manifest
+import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE
+import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+import android.content.pm.ResolveInfo
 import android.content.pm.ServiceInfo
+import android.os.UserHandle
+import android.service.controls.ControlsProviderService
+import androidx.annotation.WorkerThread
 import com.android.settingslib.applications.DefaultAppInfo
+import java.util.Objects
 
 class ControlsServiceInfo(
-    context: Context,
+    private val context: Context,
     val serviceInfo: ServiceInfo
 ) : DefaultAppInfo(
     context,
     context.packageManager,
     context.userId,
     serviceInfo.componentName
-)
\ No newline at end of file
+) {
+    private val _panelActivity: ComponentName?
+
+    init {
+        val metadata = serviceInfo.metaData
+                ?.getString(ControlsProviderService.META_DATA_PANEL_ACTIVITY) ?: ""
+        val unflatenned = ComponentName.unflattenFromString(metadata)
+        if (unflatenned != null && unflatenned.packageName == componentName.packageName) {
+            _panelActivity = unflatenned
+        } else {
+            _panelActivity = null
+        }
+    }
+
+    /**
+     * Component name of an activity that will be shown embedded in the device controls space
+     * instead of using the controls rendered by SystemUI.
+     *
+     * The activity must be in the same package, exported, enabled and protected by the
+     * [Manifest.permission.BIND_CONTROLS] permission.
+     */
+    var panelActivity: ComponentName? = null
+        private set
+
+    private var resolved: Boolean = false
+
+    @WorkerThread
+    fun resolvePanelActivity() {
+        if (resolved) return
+        resolved = true
+        panelActivity = _panelActivity?.let {
+            val resolveInfos = mPm.queryIntentActivitiesAsUser(
+                    Intent().setComponent(it),
+                    PackageManager.ResolveInfoFlags.of(
+                            MATCH_DIRECT_BOOT_AWARE.toLong() or
+                                    MATCH_DIRECT_BOOT_UNAWARE.toLong()
+                    ),
+                    UserHandle.of(userId)
+            )
+            if (resolveInfos.isNotEmpty() && verifyResolveInfo(resolveInfos[0])) {
+                it
+            } else {
+                null
+            }
+        }
+    }
+
+    /**
+     * Verifies that the panel activity is enabled, exported and protected by the correct
+     * permission. This last check is to prevent apps from forgetting to protect the activity, as
+     * they won't be able to see the panel until they do.
+     */
+    @WorkerThread
+    private fun verifyResolveInfo(resolveInfo: ResolveInfo): Boolean {
+        return resolveInfo.activityInfo?.let {
+            it.permission == Manifest.permission.BIND_CONTROLS &&
+                    it.exported && isComponentActuallyEnabled(it)
+        } ?: false
+    }
+
+    @WorkerThread
+    private fun isComponentActuallyEnabled(activityInfo: ActivityInfo): Boolean {
+        return when (mPm.getComponentEnabledSetting(activityInfo.componentName)) {
+            PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
+            PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false
+            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> activityInfo.enabled
+            else -> false
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is ControlsServiceInfo &&
+                userId == other.userId &&
+                componentName == other.componentName &&
+                panelActivity == other.panelActivity
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(userId, componentName, panelActivity)
+    }
+
+    fun copy(): ControlsServiceInfo {
+        return ControlsServiceInfo(context, serviceInfo).also {
+            it.panelActivity = this.panelActivity
+        }
+    }
+
+    override fun toString(): String {
+        return """
+            ControlsServiceInfo(serviceInfo=$serviceInfo, panelActivity=$panelActivity, resolved=$resolved)
+        """.trimIndent()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
index 2d76ff2..115edd11 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
@@ -18,17 +18,23 @@
 
 import android.content.ComponentName
 import android.content.Context
-import android.content.pm.ServiceInfo
 import android.os.UserHandle
 import android.service.controls.ControlsProviderService
 import android.util.Log
 import com.android.internal.annotations.VisibleForTesting
 import com.android.settingslib.applications.ServiceListing
 import com.android.settingslib.widget.CandidateInfo
+import com.android.systemui.Dumpable
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.indentIfPossible
+import java.io.PrintWriter
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicInteger
 import javax.inject.Inject
@@ -57,16 +63,19 @@
     private val context: Context,
     @Background private val backgroundExecutor: Executor,
     private val serviceListingBuilder: (Context) -> ServiceListing,
-    userTracker: UserTracker
-) : ControlsListingController {
+    private val userTracker: UserTracker,
+    dumpManager: DumpManager,
+    featureFlags: FeatureFlags
+) : ControlsListingController, Dumpable {
 
     @Inject
-    constructor(context: Context, executor: Executor, userTracker: UserTracker): this(
-            context,
-            executor,
-            ::createServiceListing,
-            userTracker
-    )
+    constructor(
+            context: Context,
+            @Background executor: Executor,
+            userTracker: UserTracker,
+            dumpManager: DumpManager,
+            featureFlags: FeatureFlags
+    ) : this(context, executor, ::createServiceListing, userTracker, dumpManager, featureFlags)
 
     private var serviceListing = serviceListingBuilder(context)
     // All operations in background thread
@@ -76,27 +85,25 @@
         private const val TAG = "ControlsListingControllerImpl"
     }
 
-    private var availableComponents = emptySet<ComponentName>()
-    private var availableServices = emptyList<ServiceInfo>()
+    private var availableServices = emptyList<ControlsServiceInfo>()
     private var userChangeInProgress = AtomicInteger(0)
 
     override var currentUserId = userTracker.userId
         private set
 
     private val serviceListingCallback = ServiceListing.Callback {
-        val newServices = it.toList()
-        val newComponents =
-            newServices.mapTo(mutableSetOf<ComponentName>(), { s -> s.getComponentName() })
-
         backgroundExecutor.execute {
             if (userChangeInProgress.get() > 0) return@execute
-            if (!newComponents.equals(availableComponents)) {
-                Log.d(TAG, "ServiceConfig reloaded, count: ${newComponents.size}")
-                availableComponents = newComponents
+            Log.d(TAG, "ServiceConfig reloaded, count: ${it.size}")
+            val newServices = it.map { ControlsServiceInfo(userTracker.userContext, it) }
+            if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
+                newServices.forEach(ControlsServiceInfo::resolvePanelActivity)
+            }
+
+            if (newServices != availableServices) {
                 availableServices = newServices
-                val currentServices = getCurrentServices()
                 callbacks.forEach {
-                    it.onServicesUpdated(currentServices)
+                    it.onServicesUpdated(getCurrentServices())
                 }
             }
         }
@@ -104,6 +111,7 @@
 
     init {
         Log.d(TAG, "Initializing")
+        dumpManager.registerDumpable(TAG, this)
         serviceListing.addCallback(serviceListingCallback)
         serviceListing.setListening(true)
         serviceListing.reload()
@@ -165,7 +173,7 @@
      *         [ControlsProviderService]
      */
     override fun getCurrentServices(): List<ControlsServiceInfo> =
-            availableServices.map { ControlsServiceInfo(context, it) }
+            availableServices.map(ControlsServiceInfo::copy)
 
     /**
      * Get the localized label for the component.
@@ -174,7 +182,15 @@
      * @return a label as returned by [CandidateInfo.loadLabel] or `null`.
      */
     override fun getAppLabel(name: ComponentName): CharSequence? {
-        return getCurrentServices().firstOrNull { it.componentName == name }
+        return availableServices.firstOrNull { it.componentName == name }
                 ?.loadLabel()
     }
+
+    override fun dump(writer: PrintWriter, args: Array<out String>) {
+        writer.println("ControlsListingController:")
+        writer.asIndenting().indentIfPossible {
+            println("Callbacks: $callbacks")
+            println("Services: ${getCurrentServices()}")
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
index d3555ee..7ab36e8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.dagger;
 
-import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider;
 import com.android.systemui.statusbar.QsFrameTranslateModule;
 
 import dagger.Subcomponent;
@@ -43,9 +42,4 @@
     interface Builder extends SysUIComponent.Builder {
         ReferenceSysUIComponent build();
     }
-
-    /**
-     * Member injection into the supplied argument.
-     */
-    void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 2bee75e..d0c5007 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -45,6 +45,7 @@
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeControllerImpl;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
@@ -164,7 +165,8 @@
             ConfigurationController configurationController,
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         return new HeadsUpManagerPhone(
                 context,
                 headsUpManagerLogger,
@@ -175,7 +177,8 @@
                 configurationController,
                 handler,
                 accessibilityManagerWrapper,
-                uiEventLogger
+                uiEventLogger,
+                shadeExpansionStateManager
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
index a49aaccf..95e7ad96 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
@@ -34,9 +34,6 @@
     fun isEnabled(flag: ResourceBooleanFlag): Boolean
 
     /** Returns a boolean value for the given flag.  */
-    fun isEnabled(flag: DeviceConfigBooleanFlag): Boolean
-
-    /** Returns a boolean value for the given flag.  */
     fun isEnabled(flag: SysPropBooleanFlag): Boolean
 
     /** Returns a string value for the given flag.  */
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index 910c87a..b03ae59 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -40,7 +40,6 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.settings.SecureSettings;
 
 import org.jetbrains.annotations.NotNull;
@@ -77,7 +76,6 @@
     private final SecureSettings mSecureSettings;
     private final Resources mResources;
     private final SystemPropertiesHelper mSystemProperties;
-    private final DeviceConfigProxy mDeviceConfigProxy;
     private final ServerFlagReader mServerFlagReader;
     private final Map<Integer, Flag<?>> mAllFlags;
     private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>();
@@ -100,7 +98,6 @@
             SecureSettings secureSettings,
             SystemPropertiesHelper systemProperties,
             @Main Resources resources,
-            DeviceConfigProxy deviceConfigProxy,
             ServerFlagReader serverFlagReader,
             @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
             Restarter restarter) {
@@ -109,7 +106,6 @@
         mSecureSettings = secureSettings;
         mResources = resources;
         mSystemProperties = systemProperties;
-        mDeviceConfigProxy = deviceConfigProxy;
         mServerFlagReader = serverFlagReader;
         mAllFlags = allFlags;
         mRestarter = restarter;
@@ -141,7 +137,7 @@
         int id = flag.getId();
         if (!mBooleanFlagCache.containsKey(id)) {
             mBooleanFlagCache.put(id,
-                    readFlagValue(id, flag.getDefault()));
+                    readBooleanFlagInternal(flag, flag.getDefault()));
         }
 
         return mBooleanFlagCache.get(id);
@@ -152,19 +148,7 @@
         int id = flag.getId();
         if (!mBooleanFlagCache.containsKey(id)) {
             mBooleanFlagCache.put(id,
-                    readFlagValue(id, mResources.getBoolean(flag.getResourceId())));
-        }
-
-        return mBooleanFlagCache.get(id);
-    }
-
-    @Override
-    public boolean isEnabled(@NonNull DeviceConfigBooleanFlag flag) {
-        int id = flag.getId();
-        if (!mBooleanFlagCache.containsKey(id)) {
-            boolean deviceConfigValue = mDeviceConfigProxy.getBoolean(flag.getNamespace(),
-                    flag.getName(), flag.getDefault());
-            mBooleanFlagCache.put(id, readFlagValue(id, deviceConfigValue));
+                    readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId())));
         }
 
         return mBooleanFlagCache.get(id);
@@ -180,7 +164,7 @@
                     id,
                     mSystemProperties.getBoolean(
                             flag.getName(),
-                            readFlagValue(id, flag.getDefault())));
+                            readBooleanFlagInternal(flag, flag.getDefault())));
         }
 
         return mBooleanFlagCache.get(id);
@@ -192,7 +176,7 @@
         int id = flag.getId();
         if (!mStringFlagCache.containsKey(id)) {
             mStringFlagCache.put(id,
-                    readFlagValue(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
+                    readFlagValueInternal(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
         }
 
         return mStringFlagCache.get(id);
@@ -204,20 +188,21 @@
         int id = flag.getId();
         if (!mStringFlagCache.containsKey(id)) {
             mStringFlagCache.put(id,
-                    readFlagValue(id, mResources.getString(flag.getResourceId()),
+                    readFlagValueInternal(id, mResources.getString(flag.getResourceId()),
                             StringFlagSerializer.INSTANCE));
         }
 
         return mStringFlagCache.get(id);
     }
 
+
     @NonNull
     @Override
     public int getInt(@NonNull IntFlag flag) {
         int id = flag.getId();
         if (!mIntFlagCache.containsKey(id)) {
             mIntFlagCache.put(id,
-                    readFlagValue(id, flag.getDefault(), IntFlagSerializer.INSTANCE));
+                    readFlagValueInternal(id, flag.getDefault(), IntFlagSerializer.INSTANCE));
         }
 
         return mIntFlagCache.get(id);
@@ -229,27 +214,31 @@
         int id = flag.getId();
         if (!mIntFlagCache.containsKey(id)) {
             mIntFlagCache.put(id,
-                    readFlagValue(id, mResources.getInteger(flag.getResourceId()),
+                    readFlagValueInternal(id, mResources.getInteger(flag.getResourceId()),
                             IntFlagSerializer.INSTANCE));
         }
 
         return mIntFlagCache.get(id);
     }
 
-    /** Specific override for Boolean flags that checks against the teamfood list. */
-    private boolean readFlagValue(int id, boolean defaultValue) {
-        Boolean result = readBooleanFlagOverride(id);
-        boolean hasServerOverride = mServerFlagReader.hasOverride(id);
+    /** Specific override for Boolean flags that checks against the teamfood list.*/
+    private boolean readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue) {
+        Boolean result = readBooleanFlagOverride(flag.getId());
+        boolean hasServerOverride = mServerFlagReader.hasOverride(
+                flag.getNamespace(), flag.getName());
 
         // Only check for teamfood if the default is false
         // and there is no server override.
-        if (!hasServerOverride && !defaultValue && result == null && id != Flags.TEAMFOOD.getId()) {
-            if (mAllFlags.containsKey(id) && mAllFlags.get(id).getTeamfood()) {
-                return isEnabled(Flags.TEAMFOOD);
-            }
+        if (!hasServerOverride
+                && !defaultValue
+                && result == null
+                && flag.getId() != Flags.TEAMFOOD.getId()
+                && flag.getTeamfood()) {
+            return isEnabled(Flags.TEAMFOOD);
         }
 
-        return result == null ? mServerFlagReader.readServerOverride(id, defaultValue) : result;
+        return result == null ? mServerFlagReader.readServerOverride(
+                flag.getNamespace(), flag.getName(), defaultValue) : result;
     }
 
     private Boolean readBooleanFlagOverride(int id) {
@@ -257,7 +246,8 @@
     }
 
     @NonNull
-    private <T> T readFlagValue(int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
+    private <T> T readFlagValueInternal(
+            int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
         requireNonNull(defaultValue, "defaultValue");
         T result = readFlagValueInternal(id, serializer);
         return result == null ? defaultValue : result;
@@ -356,8 +346,6 @@
             setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
         } else if (flag instanceof ResourceBooleanFlag) {
             setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
-        } else if (flag instanceof DeviceConfigBooleanFlag) {
-            setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
         } else if (flag instanceof SysPropBooleanFlag) {
             // Store SysProp flags in SystemProperties where they can read by outside parties.
             mSystemProperties.setBoolean(((SysPropBooleanFlag) flag).getName(), value);
@@ -475,9 +463,6 @@
             } else if (f instanceof ResourceBooleanFlag) {
                 enabled = isEnabled((ResourceBooleanFlag) f);
                 overridden = readBooleanFlagOverride(f.getId()) != null;
-            } else if (f instanceof DeviceConfigBooleanFlag) {
-                enabled = isEnabled((DeviceConfigBooleanFlag) f);
-                overridden = false;
             } else if (f instanceof SysPropBooleanFlag) {
                 // TODO(b/223379190): Teamfood not supported for sysprop flags yet.
                 enabled = isEnabled((SysPropBooleanFlag) f);
@@ -490,9 +475,11 @@
             }
 
             if (enabled) {
-                return new ReleasedFlag(f.getId(), teamfood, overridden);
+                return new ReleasedFlag(
+                        f.getId(), f.getName(), f.getNamespace(), teamfood, overridden);
             } else {
-                return new UnreleasedFlag(f.getId(), teamfood, overridden);
+                return new UnreleasedFlag(
+                        f.getId(), f.getName(), f.getNamespace(), teamfood, overridden);
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index 8996d52..3c83682 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -101,7 +101,7 @@
 
     @Override
     public boolean isEnabled(@NotNull ReleasedFlag flag) {
-        return mServerFlagReader.readServerOverride(flag.getId(), true);
+        return mServerFlagReader.readServerOverride(flag.getNamespace(), flag.getName(), true);
     }
 
     @Override
@@ -115,18 +115,6 @@
     }
 
     @Override
-    public boolean isEnabled(@NonNull DeviceConfigBooleanFlag flag) {
-        int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
-        if (cacheIndex < 0) {
-            boolean deviceConfigValue = mDeviceConfigProxy.getBoolean(flag.getNamespace(),
-                    flag.getName(), flag.getDefault());
-            return isEnabled(flag.getId(), deviceConfigValue);
-        }
-
-        return mBooleanCache.valueAt(cacheIndex);
-    }
-
-    @Override
     public boolean isEnabled(SysPropBooleanFlag flag) {
         int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
         if (cacheIndex < 0) {
@@ -180,10 +168,10 @@
     @Override
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("can override: false");
-        Map<Integer, Flag<?>> knownFlags = Flags.collectFlags();
-        for (Map.Entry<Integer, Flag<?>> idToFlag : knownFlags.entrySet()) {
-            int id = idToFlag.getKey();
-            Flag<?> flag = idToFlag.getValue();
+        Map<String, Flag<?>> knownFlags = FlagsFactory.INSTANCE.getKnownFlags();
+        for (Map.Entry<String, Flag<?>> nameToFlag : knownFlags.entrySet()) {
+            Flag<?> flag = nameToFlag.getValue();
+            int id = flag.getId();
             boolean def = false;
             if (mBooleanCache.indexOfKey(flag.getId()) < 0) {
                 if (flag instanceof SysPropBooleanFlag) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
index ad4b87d..b7fc0e4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
@@ -229,7 +229,7 @@
     }
 
     private int flagNameToId(String flagName) {
-        Map<String, Flag<?>> flagFields = Flags.getFlagFields();
+        Map<String, Flag<?>> flagFields = FlagsFactory.INSTANCE.getKnownFlags();
         for (String fieldName : flagFields.keySet()) {
             if (flagName.equals(fieldName)) {
                 return flagFields.get(fieldName).getId();
@@ -240,7 +240,7 @@
     }
 
     private void printKnownFlags(PrintWriter pw) {
-        Map<String, Flag<?>> fields = Flags.getFlagFields();
+        Map<String, Flag<?>> fields = FlagsFactory.INSTANCE.getKnownFlags();
 
         int longestFieldName = 0;
         for (String fieldName : fields.keySet()) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 071bbf8..99dfefa 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -17,12 +17,11 @@
 
 import android.provider.DeviceConfig
 import com.android.internal.annotations.Keep
-import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.R
-import kotlin.reflect.KClass
-import kotlin.reflect.full.declaredMembers
-import kotlin.reflect.full.isSubclassOf
-import kotlin.reflect.full.staticProperties
+import com.android.systemui.flags.FlagsFactory.releasedFlag
+import com.android.systemui.flags.FlagsFactory.resourceBooleanFlag
+import com.android.systemui.flags.FlagsFactory.sysPropBooleanFlag
+import com.android.systemui.flags.FlagsFactory.unreleasedFlag
 
 /**
  * List of [Flag] objects for use in SystemUI.
@@ -37,41 +36,51 @@
  * See [FeatureFlagsDebug] for instructions on flipping the flags via adb.
  */
 object Flags {
-    @JvmField val TEAMFOOD = UnreleasedFlag(1)
+    @JvmField val TEAMFOOD = unreleasedFlag(1, "teamfood")
 
     // 100 - notification
     // TODO(b/254512751): Tracking Bug
-    val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = UnreleasedFlag(103)
+    val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
+        unreleasedFlag(103, "notification_pipeline_developer_logging")
 
     // TODO(b/254512732): Tracking Bug
-    @JvmField val NSSL_DEBUG_LINES = UnreleasedFlag(105)
+    @JvmField val NSSL_DEBUG_LINES = unreleasedFlag(105, "nssl_debug_lines")
 
     // TODO(b/254512505): Tracking Bug
-    @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = UnreleasedFlag(106)
+    @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = unreleasedFlag(106, "nssl_debug_remove_animation")
 
     // TODO(b/254512624): Tracking Bug
     @JvmField
     val NOTIFICATION_DRAG_TO_CONTENTS =
-        ResourceBooleanFlag(108, R.bool.config_notificationToContents)
+        resourceBooleanFlag(
+            108,
+            R.bool.config_notificationToContents,
+            "notification_drag_to_contents"
+        )
 
     // TODO(b/254512517): Tracking Bug
-    val FSI_REQUIRES_KEYGUARD = UnreleasedFlag(110, teamfood = true)
+    val FSI_REQUIRES_KEYGUARD = unreleasedFlag(110, "fsi_requires_keyguard", teamfood = true)
 
     // TODO(b/254512538): Tracking Bug
-    val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true)
+    val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true)
 
     // TODO(b/254512425): Tracking Bug
-    val NOTIFICATION_MEMORY_MONITOR_ENABLED = ReleasedFlag(112)
+    val NOTIFICATION_MEMORY_MONITOR_ENABLED =
+        releasedFlag(112, "notification_memory_monitor_enabled")
 
     // TODO(b/254512731): Tracking Bug
-    @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
-    val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true)
-    val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true)
+    @JvmField
+    val NOTIFICATION_DISMISSAL_FADE =
+        unreleasedFlag(113, "notification_dismissal_fade", teamfood = true)
+    val STABILITY_INDEX_FIX = unreleasedFlag(114, "stability_index_fix", teamfood = true)
+    val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
 
-    @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, teamfood = true)
+    @JvmField
+    val NOTIFICATION_GROUP_CORNER =
+        unreleasedFlag(116, "notification_group_corner", teamfood = true)
 
     // TODO(b/257506350): Tracking Bug
-    val FSI_CHROME = UnreleasedFlag(117)
+    val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
 
     // next id: 118
 
@@ -80,25 +89,27 @@
     // public static final BooleanFlag KEYGUARD_LAYOUT =
     //         new BooleanFlag(200, true);
     // TODO(b/254512713): Tracking Bug
-    @JvmField val LOCKSCREEN_ANIMATIONS = ReleasedFlag(201)
+    @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations")
 
     // TODO(b/254512750): Tracking Bug
-    val NEW_UNLOCK_SWIPE_ANIMATION = ReleasedFlag(202)
-    val CHARGING_RIPPLE = ResourceBooleanFlag(203, R.bool.flag_charging_ripple)
+    val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation")
+    val CHARGING_RIPPLE = resourceBooleanFlag(203, R.bool.flag_charging_ripple, "charging_ripple")
 
     // TODO(b/254512281): Tracking Bug
     @JvmField
-    val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher)
+    val BOUNCER_USER_SWITCHER =
+        resourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher, "bouncer_user_switcher")
 
     // TODO(b/254512676): Tracking Bug
-    @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true)
+    @JvmField
+    val LOCKSCREEN_CUSTOM_CLOCKS = unreleasedFlag(207, "lockscreen_custom_clocks", teamfood = true)
 
     /**
      * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual
      * replacement of KeyguardBouncer.java.
      */
     // TODO(b/254512385): Tracking Bug
-    @JvmField val MODERN_BOUNCER = ReleasedFlag(208)
+    @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer")
 
     /**
      * Whether the user interactor and repository should use `UserSwitcherController`.
@@ -107,7 +118,8 @@
      * framework APIs.
      */
     // TODO(b/254513286): Tracking Bug
-    val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = UnreleasedFlag(210)
+    val USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
+        unreleasedFlag(210, "user_interactor_and_repo_use_controller")
 
     /**
      * Whether `UserSwitcherController` should use the user interactor.
@@ -119,24 +131,24 @@
      * would created a cycle between controller -> interactor -> controller.
      */
     // TODO(b/254513102): Tracking Bug
-    val USER_CONTROLLER_USES_INTERACTOR = ReleasedFlag(211)
+    val USER_CONTROLLER_USES_INTERACTOR = releasedFlag(211, "user_controller_uses_interactor")
 
     /**
      * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
      * the digits when the clock moves.
      */
-    @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212)
+    @JvmField val STEP_CLOCK_ANIMATION = unreleasedFlag(212, "step_clock_animation")
 
     /**
      * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
      * will occur in stages. This is one stage of many to come.
      */
     // TODO(b/255607168): Tracking Bug
-    @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213)
+    @JvmField val DOZING_MIGRATION_1 = unreleasedFlag(213, "dozing_migration_1")
 
-    @JvmField val NEW_ELLIPSE_DETECTION = UnreleasedFlag(214)
+    @JvmField val NEW_ELLIPSE_DETECTION = unreleasedFlag(214, "new_ellipse_detection")
 
-    @JvmField val NEW_UDFPS_OVERLAY = UnreleasedFlag(215)
+    @JvmField val NEW_UDFPS_OVERLAY = unreleasedFlag(215, "new_udfps_overlay")
 
     /**
      * Whether to enable the code powering customizable lock screen quick affordances.
@@ -144,273 +156,256 @@
      * Note that this flag does not enable individual implementations of quick affordances like the
      * new camera quick affordance. Look for individual flags for those.
      */
-    @JvmField val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = UnreleasedFlag(216, teamfood = false)
+    @JvmField
+    val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES =
+        unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false)
 
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
-    @JvmField val POWER_MENU_LITE = ReleasedFlag(300)
+    @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
 
     // 400 - smartspace
 
     // TODO(b/254513100): Tracking Bug
-    val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = ReleasedFlag(401)
-    val SMARTSPACE = ResourceBooleanFlag(402, R.bool.flag_smartspace)
+    val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
+        releasedFlag(401, "smartspace_shared_element_transition_enabled")
+    val SMARTSPACE = resourceBooleanFlag(402, R.bool.flag_smartspace, "smartspace")
 
     // 500 - quick settings
 
     // TODO(b/254512321): Tracking Bug
-    @JvmField val COMBINED_QS_HEADERS = ReleasedFlag(501)
-    val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations)
+    @JvmField val COMBINED_QS_HEADERS = releasedFlag(501, "combined_qs_headers")
+    val PEOPLE_TILE = resourceBooleanFlag(502, R.bool.flag_conversations, "people_tile")
 
     @JvmField
     val QS_USER_DETAIL_SHORTCUT =
-        ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut)
+        resourceBooleanFlag(
+            503,
+            R.bool.flag_lockscreen_qs_user_detail_shortcut,
+            "qs_user_detail_shortcut"
+        )
 
     // TODO(b/254512747): Tracking Bug
-    val NEW_HEADER = ReleasedFlag(505)
+    val NEW_HEADER = releasedFlag(505, "new_header")
 
     // TODO(b/254512383): Tracking Bug
     @JvmField
     val FULL_SCREEN_USER_SWITCHER =
-        ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher)
+        resourceBooleanFlag(
+            506,
+            R.bool.config_enableFullscreenUserSwitcher,
+            "full_screen_user_switcher"
+        )
 
     // TODO(b/254512678): Tracking Bug
-    @JvmField val NEW_FOOTER_ACTIONS = ReleasedFlag(507)
+    @JvmField val NEW_FOOTER_ACTIONS = releasedFlag(507, "new_footer_actions")
 
     // TODO(b/244064524): Tracking Bug
-    @JvmField val QS_SECONDARY_DATA_SUB_INFO = UnreleasedFlag(508, teamfood = true)
+    @JvmField
+    val QS_SECONDARY_DATA_SUB_INFO =
+        unreleasedFlag(508, "qs_secondary_data_sub_info", teamfood = true)
 
     // 600- status bar
     // TODO(b/254513246): Tracking Bug
-    val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip)
+    val STATUS_BAR_USER_SWITCHER =
+        resourceBooleanFlag(602, R.bool.flag_user_switcher_chip, "status_bar_user_switcher")
 
     // TODO(b/254512623): Tracking Bug
     @Deprecated("Replaced by mobile and wifi specific flags.")
-    val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false)
+    val NEW_STATUS_BAR_PIPELINE_BACKEND =
+        unreleasedFlag(604, "new_status_bar_pipeline_backend", teamfood = false)
 
     // TODO(b/254512660): Tracking Bug
     @Deprecated("Replaced by mobile and wifi specific flags.")
-    val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false)
+    val NEW_STATUS_BAR_PIPELINE_FRONTEND =
+        unreleasedFlag(605, "new_status_bar_pipeline_frontend", teamfood = false)
 
     // TODO(b/256614753): Tracking Bug
-    val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606)
+    val NEW_STATUS_BAR_MOBILE_ICONS = unreleasedFlag(606, "new_status_bar_mobile_icons")
 
     // TODO(b/256614210): Tracking Bug
-    val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607)
+    val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon")
 
     // TODO(b/256614751): Tracking Bug
-    val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND = UnreleasedFlag(608)
+    val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND =
+        unreleasedFlag(608, "new_status_bar_mobile_icons_backend")
 
     // TODO(b/256613548): Tracking Bug
-    val NEW_STATUS_BAR_WIFI_ICON_BACKEND = UnreleasedFlag(609)
+    val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
 
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
-    val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700)
+    val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
 
     // TODO(b/254512681): Tracking Bug
-    val ONGOING_CALL_IN_IMMERSIVE = ReleasedFlag(701)
+    val ONGOING_CALL_IN_IMMERSIVE = releasedFlag(701, "ongoing_call_in_immersive")
 
     // TODO(b/254512753): Tracking Bug
-    val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = ReleasedFlag(702)
+    val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = releasedFlag(702, "ongoing_call_in_immersive_chip_tap")
 
     // 800 - general visual/theme
-    @JvmField val MONET = ResourceBooleanFlag(800, R.bool.flag_monet)
+    @JvmField val MONET = resourceBooleanFlag(800, R.bool.flag_monet, "monet")
 
     // 801 - region sampling
     // TODO(b/254512848): Tracking Bug
-    val REGION_SAMPLING = UnreleasedFlag(801)
+    val REGION_SAMPLING = unreleasedFlag(801, "region_sampling")
 
     // 802 - wallpaper rendering
     // TODO(b/254512923): Tracking Bug
-    @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802)
+    @JvmField val USE_CANVAS_RENDERER = unreleasedFlag(802, "use_canvas_renderer")
 
     // 803 - screen contents translation
     // TODO(b/254513187): Tracking Bug
-    val SCREEN_CONTENTS_TRANSLATION = UnreleasedFlag(803)
+    val SCREEN_CONTENTS_TRANSLATION = unreleasedFlag(803, "screen_contents_translation")
 
     // 804 - monochromatic themes
     @JvmField
-    val MONOCHROMATIC_THEMES = SysPropBooleanFlag(804, "persist.sysui.monochromatic", false)
+    val MONOCHROMATIC_THEMES =
+        sysPropBooleanFlag(804, "persist.sysui.monochromatic", default = false)
 
     // 900 - media
     // TODO(b/254512697): Tracking Bug
-    val MEDIA_TAP_TO_TRANSFER = ReleasedFlag(900)
+    val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer")
 
     // TODO(b/254512502): Tracking Bug
-    val MEDIA_SESSION_ACTIONS = UnreleasedFlag(901)
+    val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions")
 
     // TODO(b/254512726): Tracking Bug
-    val MEDIA_NEARBY_DEVICES = ReleasedFlag(903)
+    val MEDIA_NEARBY_DEVICES = releasedFlag(903, "media_nearby_devices")
 
     // TODO(b/254512695): Tracking Bug
-    val MEDIA_MUTE_AWAIT = ReleasedFlag(904)
+    val MEDIA_MUTE_AWAIT = releasedFlag(904, "media_mute_await")
 
     // TODO(b/254512654): Tracking Bug
-    @JvmField val DREAM_MEDIA_COMPLICATION = UnreleasedFlag(905)
+    @JvmField val DREAM_MEDIA_COMPLICATION = unreleasedFlag(905, "dream_media_complication")
 
     // TODO(b/254512673): Tracking Bug
-    @JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906)
+    @JvmField val DREAM_MEDIA_TAP_TO_OPEN = unreleasedFlag(906, "dream_media_tap_to_open")
 
     // TODO(b/254513168): Tracking Bug
-    @JvmField val UMO_SURFACE_RIPPLE = UnreleasedFlag(907)
+    @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
 
     // 1000 - dock
-    val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000)
+    val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
 
     // TODO(b/254512758): Tracking Bug
-    @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002)
+    @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
 
     // 1100 - windowing
     @Keep
     @JvmField
     val WM_ENABLE_SHELL_TRANSITIONS =
-        SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false)
+        sysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", default = false)
 
     // TODO(b/254513207): Tracking Bug
     @Keep
     @JvmField
     val WM_ENABLE_PARTIAL_SCREEN_SHARING =
-        DeviceConfigBooleanFlag(
+        unreleasedFlag(
             1102,
-            "record_task_content",
-            DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-            false,
+            name = "record_task_content",
+            namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER,
             teamfood = true
         )
 
     // TODO(b/254512674): Tracking Bug
     @Keep
     @JvmField
-    val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false)
+    val HIDE_NAVBAR_WINDOW =
+        sysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", default = false)
 
     @Keep
     @JvmField
-    val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false)
+    val WM_DESKTOP_WINDOWING =
+        sysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", default = false)
 
     @Keep
     @JvmField
-    val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false)
+    val WM_CAPTION_ON_SHELL =
+        sysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", default = false)
 
     @Keep
     @JvmField
     val ENABLE_FLING_TO_DISMISS_BUBBLE =
-        SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true)
+        sysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", default = true)
 
     @Keep
     @JvmField
     val ENABLE_FLING_TO_DISMISS_PIP =
-        SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true)
+        sysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", default = true)
 
     @Keep
     @JvmField
     val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-        SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false)
+        sysPropBooleanFlag(
+            1110,
+            "persist.wm.debug.enable_pip_keep_clear_algorithm",
+            default = false
+        )
 
     // TODO(b/256873975): Tracking Bug
-    @JvmField @Keep val WM_BUBBLE_BAR = UnreleasedFlag(1111)
+    @JvmField @Keep val WM_BUBBLE_BAR = unreleasedFlag(1111, "wm_bubble_bar")
 
     // 1200 - predictive back
     @Keep
     @JvmField
     val WM_ENABLE_PREDICTIVE_BACK =
-        SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true)
+        sysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", default = true)
 
     @Keep
     @JvmField
     val WM_ENABLE_PREDICTIVE_BACK_ANIM =
-        SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false)
+        sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = false)
 
     @Keep
     @JvmField
     val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
-        SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false)
+        sysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", default = false)
 
     // TODO(b/254512728): Tracking Bug
-    @JvmField val NEW_BACK_AFFORDANCE = UnreleasedFlag(1203, teamfood = false)
+    @JvmField
+    val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = false)
 
     // 1300 - screenshots
     // TODO(b/254512719): Tracking Bug
-    @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300, teamfood = true)
+    @JvmField
+    val SCREENSHOT_REQUEST_PROCESSOR =
+        unreleasedFlag(1300, "screenshot_request_processor", teamfood = true)
 
     // TODO(b/254513155): Tracking Bug
-    @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301)
+    @JvmField
+    val SCREENSHOT_WORK_PROFILE_POLICY = unreleasedFlag(1301, "screenshot_work_profile_policy")
 
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
-    val QUICK_TAP_IN_PCC = ReleasedFlag(1400)
+    val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
 
     // 1500 - chooser
     // TODO(b/254512507): Tracking Bug
-    val CHOOSER_UNBUNDLED = UnreleasedFlag(1500, teamfood = true)
+    val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
 
     // 1600 - accessibility
-    @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600)
+    @JvmField
+    val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
+        unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
 
     // 1700 - clipboard
-    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700, teamfood = true)
-    @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = UnreleasedFlag(1701)
+    @JvmField
+    val CLIPBOARD_OVERLAY_REFACTOR =
+        unreleasedFlag(1700, "clipboard_overlay_refactor", teamfood = true)
+    @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = unreleasedFlag(1701, "clipboard_remote_behavior")
 
     // 1800 - shade container
-    @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, teamfood = true)
+    @JvmField
+    val LEAVE_SHADE_OPEN_FOR_BUGREPORT =
+        unreleasedFlag(1800, "leave_shade_open_for_bugreport", teamfood = true)
 
     // 1900 - note task
-    @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks")
+    @JvmField val NOTE_TASKS = sysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks")
 
     // 2000 - device controls
-    @Keep @JvmField val USE_APP_PANELS = UnreleasedFlag(2000, teamfood = true)
+    @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true)
 
     // 2100 - Falsing Manager
-    @JvmField val FALSING_FOR_LONG_TAPS = ReleasedFlag(2100)
-
-    // Pay no attention to the reflection behind the curtain.
-    // ========================== Curtain ==========================
-    // |                                                           |
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    @JvmStatic
-    fun collectFlags(): Map<Int, Flag<*>> {
-        return flagFields.mapKeys { field -> field.value.id }
-    }
-
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    @JvmStatic
-    val flagFields: Map<String, Flag<*>>
-        get() = collectFlagsInClass(Flags)
-
-    @VisibleForTesting
-    fun collectFlagsInClass(instance: Any): Map<String, Flag<*>> {
-        val cls = instance::class
-        val javaPropNames = cls.java.fields.map { it.name }
-        val props = cls.declaredMembers
-        val staticProps = cls.staticProperties
-        val staticPropNames = staticProps.map { it.name }
-        return props
-            .mapNotNull { property ->
-                if ((property.returnType.classifier as KClass<*>).isSubclassOf(Flag::class)) {
-                    // Fields with @JvmStatic should be accessed via java mechanisms
-                    if (javaPropNames.contains(property.name)) {
-                        property.name to cls.java.getField(property.name)[null] as Flag<*>
-                        // Fields with @Keep but not @JvmField. Don't do this.
-                    } else if (staticPropNames.contains(property.name)) {
-                        // The below code causes access violation exceptions. I don't know why.
-                        // property.name to (property.call() as Flag<*>)
-                        // property.name to (staticProps.find { it.name == property.name }!!
-                        // .getter.call() as Flag<*>)
-                        throw java.lang.RuntimeException(
-                            "The {$property.name} flag needs @JvmField"
-                        )
-                        // Everything else. Skip the `get` prefixed fields that kotlin adds.
-                    } else if (property.name.subSequence(0, 3) != "get") {
-                        property.name to (property.call(instance) as Flag<*>)
-                    } else {
-                        null
-                    }
-                } else {
-                    null
-                }
-            }
-            .toMap()
-    }
-    // |                                                           |
-    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
+    @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
index e1f4944..18d7bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
@@ -30,7 +30,7 @@
         @Provides
         @Named(ALL_FLAGS)
         fun providesAllFlags(): Map<Int, Flag<*>> {
-            return Flags.collectFlags()
+            return FlagsFactory.knownFlags.map { it.value.id to it.value }.toMap()
         }
 
         @JvmStatic
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
index 694fa01..ae05c46 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
@@ -27,11 +27,10 @@
 
 interface ServerFlagReader {
     /** Returns true if there is a server-side setting stored. */
-    fun hasOverride(flagId: Int): Boolean
+    fun hasOverride(namespace: String, name: String): Boolean
 
     /** Returns any stored server-side setting or the default if not set. */
-    fun readServerOverride(flagId: Int, default: Boolean): Boolean
-
+    fun readServerOverride(namespace: String, name: String, default: Boolean): Boolean
     /** Register a listener for changes to any of the passed in flags. */
     fun listenForChanges(values: Collection<Flag<*>>, listener: ChangeListener)
 
@@ -68,19 +67,19 @@
         }
     }
 
-    override fun hasOverride(flagId: Int): Boolean =
-        deviceConfig.getProperty(
+    override fun hasOverride(namespace: String, name: String): Boolean =
+        !namespace.isBlank() && !name.isBlank() && deviceConfig.getProperty(
             namespace,
-            getServerOverrideName(flagId)
+            name
         ) != null
 
-    override fun readServerOverride(flagId: Int, default: Boolean): Boolean {
-        return deviceConfig.getBoolean(
+
+    override fun readServerOverride(namespace: String, name: String, default: Boolean): Boolean =
+        !namespace.isBlank() && !name.isBlank() && deviceConfig.getBoolean(
             namespace,
-            getServerOverrideName(flagId),
+            name,
             default
         )
-    }
 
     override fun listenForChanges(
         flags: Collection<Flag<*>>,
@@ -121,24 +120,24 @@
 }
 
 class ServerFlagReaderFake : ServerFlagReader {
-    private val flagMap: MutableMap<Int, Boolean> = mutableMapOf()
+    private val flagMap: MutableMap<String, Boolean> = mutableMapOf()
     private val listeners =
         mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>()
 
-    override fun hasOverride(flagId: Int): Boolean {
-        return flagMap.containsKey(flagId)
+    override fun hasOverride(namespace: String, name: String): Boolean {
+        return flagMap.containsKey(name)
     }
 
-    override fun readServerOverride(flagId: Int, default: Boolean): Boolean {
-        return flagMap.getOrDefault(flagId, default)
+    override fun readServerOverride(namespace: String, name: String, default: Boolean): Boolean {
+        return flagMap.getOrDefault(name, default)
     }
 
-    fun setFlagValue(flagId: Int, value: Boolean) {
-        flagMap.put(flagId, value)
+    fun setFlagValue(namespace: String, name: String, value: Boolean) {
+        flagMap.put(name, value)
 
         for ((listener, flags) in listeners) {
             flagLoop@ for (flag in flags) {
-                if (flagId == flag.id) {
+                if (name == flag.name) {
                     listener.onChange()
                     break@flagLoop
                 }
@@ -146,8 +145,8 @@
         }
     }
 
-    fun eraseFlag(flagId: Int) {
-        flagMap.remove(flagId)
+    fun eraseFlag(namespace: String, name: String) {
+        flagMap.remove(name)
     }
 
     override fun listenForChanges(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
deleted file mode 100644
index 0f4581c..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard
-
-import android.content.ContentProvider
-import android.content.ContentValues
-import android.content.Context
-import android.content.UriMatcher
-import android.content.pm.ProviderInfo
-import android.database.Cursor
-import android.database.MatrixCursor
-import android.net.Uri
-import android.util.Log
-import com.android.systemui.SystemUIAppComponentFactoryBase
-import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
-import javax.inject.Inject
-import kotlinx.coroutines.runBlocking
-
-class KeyguardQuickAffordanceProvider :
-    ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
-
-    @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
-
-    private lateinit var contextAvailableCallback: ContextAvailableCallback
-
-    private val uriMatcher =
-        UriMatcher(UriMatcher.NO_MATCH).apply {
-            addURI(
-                Contract.AUTHORITY,
-                Contract.SlotTable.TABLE_NAME,
-                MATCH_CODE_ALL_SLOTS,
-            )
-            addURI(
-                Contract.AUTHORITY,
-                Contract.AffordanceTable.TABLE_NAME,
-                MATCH_CODE_ALL_AFFORDANCES,
-            )
-            addURI(
-                Contract.AUTHORITY,
-                Contract.SelectionTable.TABLE_NAME,
-                MATCH_CODE_ALL_SELECTIONS,
-            )
-        }
-
-    override fun onCreate(): Boolean {
-        return true
-    }
-
-    override fun attachInfo(context: Context?, info: ProviderInfo?) {
-        contextAvailableCallback.onContextAvailable(checkNotNull(context))
-        super.attachInfo(context, info)
-    }
-
-    override fun setContextAvailableCallback(callback: ContextAvailableCallback) {
-        contextAvailableCallback = callback
-    }
-
-    override fun getType(uri: Uri): String? {
-        val prefix =
-            when (uriMatcher.match(uri)) {
-                MATCH_CODE_ALL_SLOTS,
-                MATCH_CODE_ALL_AFFORDANCES,
-                MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
-                else -> null
-            }
-
-        val tableName =
-            when (uriMatcher.match(uri)) {
-                MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
-                MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
-                MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
-                else -> null
-            }
-
-        if (prefix == null || tableName == null) {
-            return null
-        }
-
-        return "$prefix${Contract.AUTHORITY}.$tableName"
-    }
-
-    override fun insert(uri: Uri, values: ContentValues?): Uri? {
-        if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
-            throw UnsupportedOperationException()
-        }
-
-        return insertSelection(values)
-    }
-
-    override fun query(
-        uri: Uri,
-        projection: Array<out String>?,
-        selection: String?,
-        selectionArgs: Array<out String>?,
-        sortOrder: String?,
-    ): Cursor? {
-        return when (uriMatcher.match(uri)) {
-            MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
-            MATCH_CODE_ALL_SLOTS -> querySlots()
-            MATCH_CODE_ALL_SELECTIONS -> querySelections()
-            else -> null
-        }
-    }
-
-    override fun update(
-        uri: Uri,
-        values: ContentValues?,
-        selection: String?,
-        selectionArgs: Array<out String>?,
-    ): Int {
-        Log.e(TAG, "Update is not supported!")
-        return 0
-    }
-
-    override fun delete(
-        uri: Uri,
-        selection: String?,
-        selectionArgs: Array<out String>?,
-    ): Int {
-        if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
-            throw UnsupportedOperationException()
-        }
-
-        return deleteSelection(uri, selectionArgs)
-    }
-
-    private fun insertSelection(values: ContentValues?): Uri? {
-        if (values == null) {
-            throw IllegalArgumentException("Cannot insert selection, no values passed in!")
-        }
-
-        if (!values.containsKey(Contract.SelectionTable.Columns.SLOT_ID)) {
-            throw IllegalArgumentException(
-                "Cannot insert selection, " +
-                    "\"${Contract.SelectionTable.Columns.SLOT_ID}\" not specified!"
-            )
-        }
-
-        if (!values.containsKey(Contract.SelectionTable.Columns.AFFORDANCE_ID)) {
-            throw IllegalArgumentException(
-                "Cannot insert selection, " +
-                    "\"${Contract.SelectionTable.Columns.AFFORDANCE_ID}\" not specified!"
-            )
-        }
-
-        val slotId = values.getAsString(Contract.SelectionTable.Columns.SLOT_ID)
-        val affordanceId = values.getAsString(Contract.SelectionTable.Columns.AFFORDANCE_ID)
-
-        if (slotId.isNullOrEmpty()) {
-            throw IllegalArgumentException("Cannot insert selection, slot ID was empty!")
-        }
-
-        if (affordanceId.isNullOrEmpty()) {
-            throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
-        }
-
-        val success = runBlocking {
-            interactor.select(
-                slotId = slotId,
-                affordanceId = affordanceId,
-            )
-        }
-
-        return if (success) {
-            Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
-            context?.contentResolver?.notifyChange(Contract.SelectionTable.URI, null)
-            Contract.SelectionTable.URI
-        } else {
-            Log.d(TAG, "Failed to select $affordanceId for slot $slotId")
-            null
-        }
-    }
-
-    private fun querySelections(): Cursor {
-        return MatrixCursor(
-                arrayOf(
-                    Contract.SelectionTable.Columns.SLOT_ID,
-                    Contract.SelectionTable.Columns.AFFORDANCE_ID,
-                )
-            )
-            .apply {
-                val affordanceIdsBySlotId = runBlocking { interactor.getSelections() }
-                affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
-                    affordanceIds.forEach { affordanceId ->
-                        addRow(
-                            arrayOf(
-                                slotId,
-                                affordanceId,
-                            )
-                        )
-                    }
-                }
-            }
-    }
-
-    private fun queryAffordances(): Cursor {
-        return MatrixCursor(
-                arrayOf(
-                    Contract.AffordanceTable.Columns.ID,
-                    Contract.AffordanceTable.Columns.NAME,
-                    Contract.AffordanceTable.Columns.ICON,
-                )
-            )
-            .apply {
-                interactor.getAffordancePickerRepresentations().forEach { representation ->
-                    addRow(
-                        arrayOf(
-                            representation.id,
-                            representation.name,
-                            representation.iconResourceId,
-                        )
-                    )
-                }
-            }
-    }
-
-    private fun querySlots(): Cursor {
-        return MatrixCursor(
-                arrayOf(
-                    Contract.SlotTable.Columns.ID,
-                    Contract.SlotTable.Columns.CAPACITY,
-                )
-            )
-            .apply {
-                interactor.getSlotPickerRepresentations().forEach { representation ->
-                    addRow(
-                        arrayOf(
-                            representation.id,
-                            representation.maxSelectedAffordances,
-                        )
-                    )
-                }
-            }
-    }
-
-    private fun deleteSelection(
-        uri: Uri,
-        selectionArgs: Array<out String>?,
-    ): Int {
-        if (selectionArgs == null) {
-            throw IllegalArgumentException(
-                "Cannot delete selection, selection arguments not included!"
-            )
-        }
-
-        val (slotId, affordanceId) =
-            when (selectionArgs.size) {
-                1 -> Pair(selectionArgs[0], null)
-                2 -> Pair(selectionArgs[0], selectionArgs[1])
-                else ->
-                    throw IllegalArgumentException(
-                        "Cannot delete selection, selection arguments has wrong size, expected to" +
-                            " have 1 or 2 arguments, had ${selectionArgs.size} instead!"
-                    )
-            }
-
-        val deleted = runBlocking {
-            interactor.unselect(
-                slotId = slotId,
-                affordanceId = affordanceId,
-            )
-        }
-
-        return if (deleted) {
-            Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
-            context?.contentResolver?.notifyChange(uri, null)
-            1
-        } else {
-            Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId")
-            0
-        }
-    }
-
-    companion object {
-        private const val TAG = "KeyguardQuickAffordanceProvider"
-        private const val MATCH_CODE_ALL_SLOTS = 1
-        private const val MATCH_CODE_ALL_AFFORDANCES = 2
-        private const val MATCH_CODE_ALL_SELECTIONS = 3
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
deleted file mode 100644
index fc61e90..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.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 7b5085a..a87fec3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -124,6 +124,7 @@
 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;
@@ -463,6 +464,7 @@
     private boolean mCollapsedOnDown;
     private boolean mClosingWithAlphaFadeOut;
     private boolean mHeadsUpAnimatingAway;
+    private boolean mLaunchingAffordance;
     private final FalsingManager mFalsingManager;
     private final FalsingCollector mFalsingCollector;
 
@@ -573,7 +575,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;
@@ -613,6 +615,7 @@
     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;
@@ -740,6 +743,7 @@
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             ShadeTransitionController shadeTransitionController,
             SystemClock systemClock,
+            CameraGestureHelper cameraGestureHelper,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
             DumpManager dumpManager) {
@@ -920,6 +924,7 @@
                         unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay);
                     }
                 });
+        mCameraGestureHelper = cameraGestureHelper;
         mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
         dumpManager.registerDumpable(this);
     }
@@ -2809,7 +2814,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);
@@ -2818,10 +2823,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();
             }
@@ -2858,12 +2863,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,
@@ -2898,8 +2901,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
@@ -3389,11 +3392,7 @@
         boolean isExpanded = !isFullyCollapsed() || mExpectingSynthesizedDown;
         if (mPanelExpanded != isExpanded) {
             mPanelExpanded = isExpanded;
-
-            mHeadsUpManager.setIsPanelExpanded(isExpanded);
-            mStatusBarTouchableRegionManager.setPanelExpanded(isExpanded);
-            mCentralSurfaces.setPanelExpanded(isExpanded);
-
+            mShadeExpansionStateManager.onShadeExpansionFullyChanged(isExpanded);
             if (!isExpanded && mQs != null && mQs.isCustomizing()) {
                 mQs.closeCustomizer();
             }
@@ -3947,10 +3946,6 @@
         }
     }
 
-    public int getBarState() {
-        return mBarState;
-    }
-
     private boolean isOnKeyguard() {
         return mBarState == KEYGUARD;
     }
@@ -3996,6 +3991,35 @@
                 && 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;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 66a22f4..b719177 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -44,6 +44,7 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManagerGlobal;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
@@ -158,6 +159,7 @@
                         SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER);
         configurationController.addCallback(this);
         shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         float desiredPreferredRefreshRate = context.getResources()
                 .getInteger(R.integer.config_keyguardRefreshRate);
@@ -204,6 +206,14 @@
         }
     }
 
+    @VisibleForTesting
+    void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        if (mCurrentState.mPanelExpanded != isExpanded) {
+            mCurrentState.mPanelExpanded = isExpanded;
+            apply(mCurrentState);
+        }
+    }
+
     /**
      * Register a listener to monitor scrims visibility
      * @param listener A listener to monitor scrims visibility
@@ -699,15 +709,6 @@
     }
 
     @Override
-    public void setPanelExpanded(boolean isExpanded) {
-        if (mCurrentState.mPanelExpanded == isExpanded) {
-            return;
-        }
-        mCurrentState.mPanelExpanded = isExpanded;
-        apply(mCurrentState);
-    }
-
-    @Override
     public void onRemoteInputActive(boolean remoteInputActive) {
         mCurrentState.mRemoteInputActive = remoteInputActive;
         apply(mCurrentState);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 667392c..a1767cc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -34,6 +34,7 @@
 class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents {
 
     private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
+    private val fullExpansionListeners = CopyOnWriteArrayList<ShadeFullExpansionListener>()
     private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>()
     private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
     private val shadeStateEventsListeners = CopyOnWriteArrayList<ShadeStateEventsListener>()
@@ -62,6 +63,15 @@
         expansionListeners.remove(listener)
     }
 
+    fun addFullExpansionListener(listener: ShadeFullExpansionListener) {
+        fullExpansionListeners.add(listener)
+        listener.onShadeExpansionFullyChanged(qsExpanded)
+    }
+
+    fun removeFullExpansionListener(listener: ShadeFullExpansionListener) {
+        fullExpansionListeners.remove(listener)
+    }
+
     fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
         qsExpansionListeners.add(listener)
         listener.onQsExpansionChanged(qsExpanded)
@@ -156,6 +166,13 @@
         qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
     }
 
+    fun onShadeExpansionFullyChanged(isExpanded: Boolean) {
+        this.expanded = isExpanded
+
+        debugLog("expanded=$isExpanded")
+        fullExpansionListeners.forEach { it.onShadeExpansionFullyChanged(isExpanded) }
+    }
+
     /** Updates the panel state if necessary. */
     fun updateState(@PanelState state: Int) {
         debugLog(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt
new file mode 100644
index 0000000..6d13e19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+/** A listener interface to be notified of expansion events for the notification shade. */
+fun interface ShadeFullExpansionListener {
+    /** Invoked whenever the shade expansion changes, when it is fully collapsed or expanded */
+    fun onShadeExpansionFullyChanged(isExpanded: Boolean)
+}
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/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index e21acb7..0b1807d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -123,9 +123,6 @@
     /** Sets whether the window was collapsed by force or not. */
     default void setForceWindowCollapsed(boolean force) {}
 
-    /** Sets whether panel is expanded or not. */
-    default void setPanelExpanded(boolean isExpanded) {}
-
     /** Gets whether the panel is expanded or not. */
     default boolean getPanelExpanded() {
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index fdec745..58ce447 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -53,6 +53,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.policy.CallbackController;
 
@@ -152,13 +153,18 @@
     private Interpolator mDozeInterpolator = Interpolators.FAST_OUT_SLOW_IN;
 
     @Inject
-    public StatusBarStateControllerImpl(UiEventLogger uiEventLogger, DumpManager dumpManager,
-            InteractionJankMonitor interactionJankMonitor) {
+    public StatusBarStateControllerImpl(
+            UiEventLogger uiEventLogger,
+            DumpManager dumpManager,
+            InteractionJankMonitor interactionJankMonitor,
+            ShadeExpansionStateManager shadeExpansionStateManager
+    ) {
         mUiEventLogger = uiEventLogger;
         mInteractionJankMonitor = interactionJankMonitor;
         for (int i = 0; i < HISTORY_SIZE; i++) {
             mHistoricalRecords[i] = new HistoricalState();
         }
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         dumpManager.registerDumpable(this);
     }
@@ -262,21 +268,6 @@
     }
 
     @Override
-    public boolean setPanelExpanded(boolean expanded) {
-        if (mIsExpanded == expanded) {
-            return false;
-        }
-        mIsExpanded = expanded;
-        String tag = getClass().getSimpleName() + "#setIsExpanded";
-        DejankUtils.startDetectingBlockingIpcs(tag);
-        for (RankedListener rl : new ArrayList<>(mListeners)) {
-            rl.mListener.onExpandedChanged(mIsExpanded);
-        }
-        DejankUtils.stopDetectingBlockingIpcs(tag);
-        return true;
-    }
-
-    @Override
     public float getInterpolatedDozeAmount() {
         return mDozeInterpolator.getInterpolation(mDozeAmount);
     }
@@ -324,6 +315,18 @@
         }
     }
 
+    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        if (mIsExpanded != isExpanded) {
+            mIsExpanded = isExpanded;
+            String tag = getClass().getSimpleName() + "#setIsExpanded";
+            DejankUtils.startDetectingBlockingIpcs(tag);
+            for (RankedListener rl : new ArrayList<>(mListeners)) {
+                rl.mListener.onExpandedChanged(mIsExpanded);
+            }
+            DejankUtils.stopDetectingBlockingIpcs(tag);
+        }
+    }
+
     private void startDozeAnimation() {
         if (mDozeAmount == 0f || mDozeAmount == 1f) {
             mDozeInterpolator = mIsDozing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index 1189107..5a392a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -109,13 +109,6 @@
     void setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated);
 
     /**
-     * Update the expanded state from {@link CentralSurfaces}'s perspective
-     * @param expanded are we expanded?
-     * @return {@code true} if the state changed, else {@code false}
-     */
-    boolean setPanelExpanded(boolean expanded);
-
-    /**
      * Sets whether to leave status bar open when hiding keyguard
      */
     void setLeaveOpenOnKeyguardHide(boolean leaveOpen);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index ff63891..df2de56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -34,6 +34,7 @@
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeEventsModule;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
 import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
@@ -160,6 +161,7 @@
             NotificationVisibilityProvider visibilityProvider,
             NotifPipeline notifPipeline,
             StatusBarStateController statusBarStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             NotificationLogger.ExpansionStateLogger expansionStateLogger,
             NotificationPanelLogger notificationPanelLogger) {
         return new NotificationLogger(
@@ -169,6 +171,7 @@
                 visibilityProvider,
                 notifPipeline,
                 statusBarStateController,
+                shadeExpansionStateManager,
                 expansionStateLogger,
                 notificationPanelLogger);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 6391877..58f59be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -36,6 +36,7 @@
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
@@ -92,25 +93,6 @@
     private Boolean mPanelExpanded = null;  // Use null to indicate state is not yet known
     private boolean mLogging = false;
 
-    protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
-            new OnChildLocationsChangedListener() {
-                @Override
-                public void onChildLocationsChanged() {
-                    if (mHandler.hasCallbacks(mVisibilityReporter)) {
-                        // Visibilities will be reported when the existing
-                        // callback is executed.
-                        return;
-                    }
-                    // Calculate when we're allowed to run the visibility
-                    // reporter. Note that this timestamp might already have
-                    // passed. That's OK, the callback will just be executed
-                    // ASAP.
-                    long nextReportUptimeMs =
-                            mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
-                    mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
-                }
-            };
-
     // Tracks notifications currently visible in mNotificationStackScroller and
     // emits visibility events via NoMan on changes.
     protected Runnable mVisibilityReporter = new Runnable() {
@@ -219,6 +201,7 @@
             NotificationVisibilityProvider visibilityProvider,
             NotifPipeline notifPipeline,
             StatusBarStateController statusBarStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             ExpansionStateLogger expansionStateLogger,
             NotificationPanelLogger notificationPanelLogger) {
         mNotificationListener = notificationListener;
@@ -232,6 +215,7 @@
         mNotificationPanelLogger = notificationPanelLogger;
         // Not expected to be destroyed, don't need to unsubscribe
         statusBarStateController.addCallback(this);
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         registerNewPipelineListener();
     }
@@ -278,14 +262,14 @@
             if (DEBUG) {
                 Log.i(TAG, "startNotificationLogging");
             }
-            mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
+            mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged);
             // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
             // cause the scroller to emit child location events. Hence generate
             // one ourselves to guarantee that we're reporting visible
             // notifications.
             // (Note that in cases where the scroller does emit events, this
             // additional event doesn't break anything.)
-            mNotificationLocationsChangedListener.onChildLocationsChanged();
+            onChildLocationsChanged();
         }
     }
 
@@ -411,21 +395,6 @@
     }
 
     /**
-     * Called by CentralSurfaces to notify the logger that the panel expansion has changed.
-     * The panel may be showing any of the normal notification panel, the AOD, or the bouncer.
-     * @param isExpanded True if the panel is expanded.
-     */
-    public void onPanelExpandedChanged(boolean isExpanded) {
-        if (DEBUG) {
-            Log.i(TAG, "onPanelExpandedChanged: new=" + isExpanded);
-        }
-        mPanelExpanded = isExpanded;
-        synchronized (mDozingLock) {
-            maybeUpdateLoggingStatus();
-        }
-    }
-
-    /**
      * Called when the notification is expanded / collapsed.
      */
     public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) {
@@ -434,6 +403,36 @@
     }
 
     @VisibleForTesting
+    void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        // mPanelExpanded is initialized as null
+        if (mPanelExpanded == null || !mPanelExpanded.equals(isExpanded)) {
+            if (DEBUG) {
+                Log.i(TAG, "onPanelExpandedChanged: new=" + isExpanded);
+            }
+            mPanelExpanded = isExpanded;
+            synchronized (mDozingLock) {
+                maybeUpdateLoggingStatus();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void onChildLocationsChanged() {
+        if (mHandler.hasCallbacks(mVisibilityReporter)) {
+            // Visibilities will be reported when the existing
+            // callback is executed.
+            return;
+        }
+        // Calculate when we're allowed to run the visibility
+        // reporter. Note that this timestamp might already have
+        // passed. That's OK, the callback will just be executed
+        // ASAP.
+        long nextReportUptimeMs =
+                mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
+        mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
+    }
+
+    @VisibleForTesting
     public void setVisibilityReporter(Runnable visibilityReporter) {
         mVisibilityReporter = visibilityReporter;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index de158c4..3021414 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1511,7 +1511,7 @@
             l.setAlpha(alpha);
         }
         if (mChildrenContainer != null) {
-            mChildrenContainer.setAlpha(alpha);
+            mChildrenContainer.setContentAlpha(alpha);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 0554fb5..645a02d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -495,6 +495,20 @@
     }
 
     /**
+     * Sets the alpha on the content, while leaving the background of the container itself as is.
+     *
+     * @param alpha alpha value to apply to the content
+     */
+    public void setContentAlpha(float alpha) {
+        for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+            mNotificationHeader.getChildAt(i).setAlpha(alpha);
+        }
+        for (ExpandableNotificationRow child : getAttachedChildren()) {
+            child.setContentAlpha(alpha);
+        }
+    }
+
+    /**
      * To be called any time the rows have been updated
      */
     public void updateExpansionStates() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 2359e87..1ab9be7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -258,8 +258,6 @@
 
     void onKeyguardViewManagerStatesUpdated();
 
-    void setPanelExpanded(boolean isExpanded);
-
     ViewGroup getNotificationScrollLayout();
 
     boolean isPulsing();
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 f3482f4..41f0520 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -55,7 +55,6 @@
 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;
@@ -72,8 +71,6 @@
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 /** */
 @CentralSurfacesComponent.CentralSurfacesScope
 public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callbacks {
@@ -102,7 +99,6 @@
     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);
@@ -132,8 +128,8 @@
             Optional<Vibrator> vibratorOptional,
             DisableFlagsLogger disableFlagsLogger,
             @DisplayId int displayId,
-            SystemBarAttributesListener systemBarAttributesListener,
-            Lazy<CameraLauncher> cameraLauncherLazy) {
+            SystemBarAttributesListener systemBarAttributesListener) {
+
         mCentralSurfaces = centralSurfaces;
         mContext = context;
         mShadeController = shadeController;
@@ -156,7 +152,6 @@
         mVibratorOptional = vibratorOptional;
         mDisableFlagsLogger = disableFlagsLogger;
         mDisplayId = displayId;
-        mCameraLauncherLazy = cameraLauncherLazy;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -351,8 +346,7 @@
             mCentralSurfaces.setLaunchCameraOnFinishedGoingToSleep(true);
             return;
         }
-        if (!mCameraLauncherLazy.get().canCameraGestureBeLaunched(
-                mNotificationPanelViewController.getBarState())) {
+        if (!mNotificationPanelViewController.canCameraGestureBeLaunched()) {
             if (CentralSurfaces.DEBUG_CAMERA_LIFT) {
                 Slog.d(CentralSurfaces.TAG, "Can't launch camera right now");
             }
@@ -389,8 +383,7 @@
                 if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                     mStatusBarKeyguardViewManager.reset(true /* hide */);
                 }
-                mCameraLauncherLazy.get().launchCamera(source,
-                        mNotificationPanelViewController.isFullyCollapsed());
+                mNotificationPanelViewController.launchCamera(source);
                 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 cfee305..71609f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -175,7 +175,6 @@
 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;
@@ -469,7 +468,6 @@
     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;
@@ -602,7 +600,6 @@
 
     private Runnable mLaunchTransitionEndRunnable;
     private Runnable mLaunchTransitionCancelRunnable;
-    private boolean mLaunchingAffordance;
     private boolean mLaunchCameraWhenFinishedWaking;
     private boolean mLaunchCameraOnFinishedGoingToSleep;
     private boolean mLaunchEmergencyActionWhenFinishedWaking;
@@ -747,8 +744,7 @@
             InteractionJankMonitor jankMonitor,
             DeviceStateManager deviceStateManager,
             WiredChargingRippleController wiredChargingRippleController,
-            IDreamManager dreamManager,
-            Lazy<CameraLauncher> cameraLauncherLazy) {
+            IDreamManager dreamManager) {
         mContext = context;
         mNotificationsController = notificationsController;
         mFragmentService = fragmentService;
@@ -825,7 +821,6 @@
         mMessageRouter = messageRouter;
         mWallpaperManager = wallpaperManager;
         mJankMonitor = jankMonitor;
-        mCameraLauncherLazy = cameraLauncherLazy;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
@@ -836,6 +831,7 @@
         mScreenOffAnimationController = screenOffAnimationController;
 
         mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
+        mShadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         mBubbleExpandListener = (isExpanding, key) ->
                 mContext.getMainExecutor().execute(this::updateScrimController);
@@ -1364,6 +1360,7 @@
     private void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
         float fraction = event.getFraction();
         boolean tracking = event.getTracking();
+        boolean isExpanded = event.getExpanded();
         dispatchPanelExpansionForKeyguardDismiss(fraction, tracking);
 
         if (fraction == 0 || fraction == 1) {
@@ -1376,6 +1373,23 @@
         }
     }
 
+    @VisibleForTesting
+    void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        if (mPanelExpanded != isExpanded) {
+            mPanelExpanded = isExpanded;
+            if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
+                if (DEBUG) {
+                    Log.v(TAG, "clearing notification effects from Height");
+                }
+                clearNotificationEffects();
+            }
+
+            if (!isExpanded) {
+                mRemoteInputManager.onPanelCollapsed();
+            }
+        }
+    }
+
     @NonNull
     @Override
     public Lifecycle getLifecycle() {
@@ -1780,27 +1794,6 @@
     }
 
     @Override
-    public void setPanelExpanded(boolean isExpanded) {
-        if (mPanelExpanded != isExpanded) {
-            mNotificationLogger.onPanelExpandedChanged(isExpanded);
-        }
-        mPanelExpanded = isExpanded;
-        mStatusBarHideIconsForBouncerManager.setPanelExpandedAndTriggerUpdate(isExpanded);
-        mNotificationShadeWindowController.setPanelExpanded(isExpanded);
-        mStatusBarStateController.setPanelExpanded(isExpanded);
-        if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
-            if (DEBUG) {
-                Log.v(TAG, "clearing notification effects from Height");
-            }
-            clearNotificationEffects();
-        }
-
-        if (!isExpanded) {
-            mRemoteInputManager.onPanelCollapsed();
-        }
-    }
-
-    @Override
     public ViewGroup getNotificationScrollLayout() {
         return mStackScroller;
     }
@@ -2956,7 +2949,7 @@
 
     private void onLaunchTransitionFadingEnded() {
         mNotificationPanelViewController.resetAlpha();
-        mCameraLauncherLazy.get().setLaunchingAffordance(false);
+        mNotificationPanelViewController.onAffordanceLaunchEnded();
         releaseGestureWakeLock();
         runLaunchTransitionEndRunnable();
         mKeyguardStateController.setLaunchTransitionFadingAway(false);
@@ -3026,7 +3019,7 @@
 
     private void onLaunchTransitionTimeout() {
         Log.w(TAG, "Launch transition: Timeout!");
-        mCameraLauncherLazy.get().setLaunchingAffordance(false);
+        mNotificationPanelViewController.onAffordanceLaunchEnded();
         releaseGestureWakeLock();
         mNotificationPanelViewController.resetViews(false /* animate */);
     }
@@ -3079,7 +3072,7 @@
         }
         mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         releaseGestureWakeLock();
-        mCameraLauncherLazy.get().setLaunchingAffordance(false);
+        mNotificationPanelViewController.onAffordanceLaunchEnded();
         mNotificationPanelViewController.resetAlpha();
         mNotificationPanelViewController.resetTranslation();
         mNotificationPanelViewController.resetViewGroupFade();
@@ -3237,7 +3230,7 @@
     @Override
     public void endAffordanceLaunch() {
         releaseGestureWakeLock();
-        mCameraLauncherLazy.get().setLaunchingAffordance(false);
+        mNotificationPanelViewController.onAffordanceLaunchEnded();
     }
 
     /**
@@ -3510,7 +3503,7 @@
     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
         @Override
         public void onFinishedGoingToSleep() {
-            mCameraLauncherLazy.get().setLaunchingAffordance(false);
+            mNotificationPanelViewController.onAffordanceLaunchEnded();
             releaseGestureWakeLock();
             mLaunchCameraWhenFinishedWaking = false;
             mDeviceInteractive = false;
@@ -3611,8 +3604,7 @@
                         .updateSensitivenessForOccludedWakeup();
             }
             if (mLaunchCameraWhenFinishedWaking) {
-                mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource,
-                        mNotificationPanelViewController.isFullyCollapsed());
+                mNotificationPanelViewController.launchCamera(mLastCameraLaunchSource);
                 mLaunchCameraWhenFinishedWaking = false;
             }
             if (mLaunchEmergencyActionWhenFinishedWaking) {
@@ -3803,7 +3795,8 @@
 
         mScrimController.setExpansionAffectsAlpha(!unlocking);
 
-        boolean launchingAffordanceWithPreview = mLaunchingAffordance;
+        boolean launchingAffordanceWithPreview =
+                mNotificationPanelViewController.isLaunchingAffordanceWithPreview();
         mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview);
 
         if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 103e4f6..3743fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -34,6 +34,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
@@ -111,7 +112,8 @@
             ConfigurationController configurationController,
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         super(context, logger, handler, accessibilityManagerWrapper, uiEventLogger);
         Resources resources = mContext.getResources();
         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
@@ -132,6 +134,8 @@
                 updateResources();
             }
         });
+
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
     }
 
     public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -221,13 +225,7 @@
         mTrackingHeadsUp = trackingHeadsUp;
     }
 
-    /**
-     * Notify that the status bar panel gets expanded or collapsed.
-     *
-     * @param isExpanded True to notify expanded, false to notify collapsed.
-     * TODO(b/237811427) replace with a listener
-     */
-    public void setIsPanelExpanded(boolean isExpanded) {
+    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
         if (isExpanded != mIsExpanded) {
             mIsExpanded = isExpanded;
             if (isExpanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
index 5113191..4d9de09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
@@ -5,6 +5,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -24,10 +25,11 @@
  */
 @SysUISingleton
 class StatusBarHideIconsForBouncerManager @Inject constructor(
-    private val commandQueue: CommandQueue,
-    @Main private val mainExecutor: DelayableExecutor,
-    statusBarWindowStateController: StatusBarWindowStateController,
-    dumpManager: DumpManager
+        private val commandQueue: CommandQueue,
+        @Main private val mainExecutor: DelayableExecutor,
+        statusBarWindowStateController: StatusBarWindowStateController,
+        shadeExpansionStateManager: ShadeExpansionStateManager,
+        dumpManager: DumpManager
 ) : Dumpable {
     // State variables set by external classes.
     private var panelExpanded: Boolean = false
@@ -47,6 +49,12 @@
         statusBarWindowStateController.addListener {
                 state -> setStatusBarStateAndTriggerUpdate(state)
         }
+        shadeExpansionStateManager.addFullExpansionListener { isExpanded ->
+            if (panelExpanded != isExpanded) {
+                panelExpanded = isExpanded
+                updateHideIconsForBouncer(animate = false)
+            }
+        }
     }
 
     /** Returns true if the status bar icons should be hidden in the bouncer. */
@@ -63,11 +71,6 @@
         this.displayId = displayId
     }
 
-    fun setPanelExpandedAndTriggerUpdate(panelExpanded: Boolean) {
-        this.panelExpanded = panelExpanded
-        updateHideIconsForBouncer(animate = false)
-    }
-
     fun setIsOccludedAndTriggerUpdate(isOccluded: Boolean) {
         this.isOccluded = isOccluded
         updateHideIconsForBouncer(animate = false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index d9c0293..2a039da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -34,6 +34,7 @@
 import com.android.systemui.R;
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -68,12 +69,15 @@
     private int mDisplayCutoutTouchableRegionSize;
     private int mStatusBarHeight;
 
+    private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
+
     @Inject
     public StatusBarTouchableRegionManager(
             Context context,
             NotificationShadeWindowController notificationShadeWindowController,
             ConfigurationController configurationController,
             HeadsUpManagerPhone headsUpManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
     ) {
         mContext = context;
@@ -101,17 +105,7 @@
                         updateTouchableRegion();
                     }
                 });
-        mHeadsUpManager.addHeadsUpPhoneListener(
-                new HeadsUpManagerPhone.OnHeadsUpPhoneListenerChange() {
-                    @Override
-                    public void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) {
-                        if (!headsUpGoingAway) {
-                            updateTouchableRegionAfterLayout();
-                        } else {
-                            updateTouchableRegion();
-                        }
-                    }
-                });
+        mHeadsUpManager.addHeadsUpPhoneListener(this::onHeadsUpGoingAwayStateChanged);
 
         mNotificationShadeWindowController = notificationShadeWindowController;
         mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> {
@@ -119,6 +113,9 @@
         });
 
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+
+        mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
     }
 
     protected void setup(
@@ -136,17 +133,11 @@
         pw.println(mTouchableRegion);
     }
 
-    /**
-     * Notify that the status bar panel gets expanded or collapsed.
-     *
-     * @param isExpanded True to notify expanded, false to notify collapsed.
-     * TODO(b/237811427) replace with a listener
-     */
-    public void setPanelExpanded(boolean isExpanded) {
+    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
         if (isExpanded != mIsStatusBarExpanded) {
             mIsStatusBarExpanded = isExpanded;
             if (isExpanded) {
-                // make sure our state is sane
+                // make sure our state is sensible
                 mForceCollapsedUntilLayout = false;
             }
             updateTouchableRegion();
@@ -260,18 +251,22 @@
                 || mUnlockedScreenOffAnimationController.isAnimationPlaying();
     }
 
-    private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener =
-            new OnComputeInternalInsetsListener() {
-        @Override
-        public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
-            if (shouldMakeEntireScreenTouchable()) {
-                return;
-            }
-
-            // Update touch insets to include any area needed for touching features that live in
-            // the status bar (ie: heads up notifications)
-            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-            info.touchableRegion.set(calculateTouchableRegion());
+    private void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) {
+        if (!headsUpGoingAway) {
+            updateTouchableRegionAfterLayout();
+        } else {
+            updateTouchableRegion();
         }
-    };
+    }
+
+    private void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+        if (shouldMakeEntireScreenTouchable()) {
+            return;
+        }
+
+        // Update touch insets to include any area needed for touching features that live in
+        // the status bar (ie: heads up notifications)
+        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        info.touchableRegion.set(calculateTouchableRegion());
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 4cb41f3..8270336 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -65,8 +65,7 @@
         height = WindowManager.LayoutParams.WRAP_CONTENT
         type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
         flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
-            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
-            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
         format = PixelFormat.TRANSLUCENT
         setTrustedOverlay()
     }
@@ -120,20 +119,27 @@
             // At this point, we're guaranteed to no longer be displaying a view.
             // So, set up all our callbacks and inflate the view.
             configurationController.addCallback(displayScaleListener)
-            // Wake the screen if necessary so the user will see the view. (Per b/239426653, we want
-            // the view to show over the dream state, so we should only wake up if the screen is
-            // completely off.)
-            if (!powerManager.isScreenOn) {
-                wakeLock = wakeLockBuilder
+
+            wakeLock = if (!powerManager.isScreenOn) {
+                // If the screen is off, fully wake it so the user can see the view.
+                wakeLockBuilder
                     .setTag(newInfo.windowTitle)
                     .setLevelsAndFlags(
-                        PowerManager.FULL_WAKE_LOCK or
-                        PowerManager.ACQUIRE_CAUSES_WAKEUP
+                            PowerManager.FULL_WAKE_LOCK or
+                                PowerManager.ACQUIRE_CAUSES_WAKEUP
                     )
                     .build()
-                wakeLock?.acquire(newInfo.wakeReason)
-                wakeReasonAcquired = newInfo.wakeReason
+            } else {
+                // Per b/239426653, we want the view to show over the dream state.
+                // If the screen is on, using screen bright level will leave screen on the dream
+                // state but ensure the screen will not go off before wake lock is released.
+                wakeLockBuilder
+                    .setTag(newInfo.windowTitle)
+                    .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
+                    .build()
             }
+            wakeLock?.acquire(newInfo.wakeReason)
+            wakeReasonAcquired = newInfo.wakeReason
             logger.logViewAddition(newInfo.windowTitle)
             inflateAndUpdateView(newInfo)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 61eadeb..5ea4399 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -48,6 +48,7 @@
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeControllerImpl;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -159,7 +160,8 @@
             ConfigurationController configurationController,
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         return new HeadsUpManagerPhone(
                 context,
                 headsUpManagerLogger,
@@ -170,7 +172,8 @@
                 configurationController,
                 handler,
                 accessibilityManagerWrapper,
-                uiEventLogger
+                uiEventLogger,
+                shadeExpansionStateManager
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 903aba1..2c64fe1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -1230,6 +1230,9 @@
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
                 break;
             case RINGER_MODE_VIBRATE:
+                // Feedback handled by onStateChange, for feedback both when user toggles
+                // directly in volume dialog, or drags slider to a value of 0 in settings.
+                break;
             default:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
         }
@@ -1630,9 +1633,8 @@
                 && mState.ringerModeInternal != -1
                 && mState.ringerModeInternal != state.ringerModeInternal
                 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
-            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK));
+            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
         }
-
         mState = state;
         mDynamic.clear();
         // add any new dynamic rows
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 45b8ce1..12c2bbf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -175,6 +175,25 @@
     }
 
     @Test
+    fun testFocusLossAfterRotating() {
+        val container = initializeFingerprintContainer()
+        waitForIdleSync()
+
+        val requestID = authContainer?.requestId ?: 0L
+
+        verify(callback).onDialogAnimatedIn(requestID)
+        container.onOrientationChanged()
+        container.onWindowFocusChanged(false)
+        waitForIdleSync()
+
+        verify(callback, never()).onDismissed(
+                eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
+                eq<ByteArray?>(null), /* credentialAttestation */
+                eq(requestID)
+        )
+    }
+
+    @Test
     fun testDismissesOnFocusLoss_hidesKeyboardWhenVisible() {
         val container = initializeFingerprintContainer(
             authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
index db41d8d..dedc723 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
@@ -16,27 +16,42 @@
 
 package com.android.systemui.controls.management
 
+import android.Manifest
 import android.content.ComponentName
 import android.content.Context
 import android.content.ContextWrapper
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
 import android.content.pm.ServiceInfo
+import android.os.Bundle
 import android.os.UserHandle
+import android.service.controls.ControlsProviderService
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.settingslib.applications.ServiceListing
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.USE_APP_PANELS
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argThat
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
@@ -51,10 +66,8 @@
 class ControlsListingControllerImplTest : SysuiTestCase() {
 
     companion object {
-        private const val TEST_LABEL = "TEST_LABEL"
-        private const val TEST_PERMISSION = "permission"
-        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
-        fun <T> any(): T = Mockito.any<T>()
+        private const val FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or
+                PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong()
     }
 
     @Mock
@@ -63,15 +76,17 @@
     private lateinit var mockCallback: ControlsListingController.ControlsListingCallback
     @Mock
     private lateinit var mockCallbackOther: ControlsListingController.ControlsListingCallback
-    @Mock
-    private lateinit var serviceInfo: ServiceInfo
-    @Mock
-    private lateinit var serviceInfo2: ServiceInfo
     @Mock(stubOnly = true)
     private lateinit var userTracker: UserTracker
+    @Mock(stubOnly = true)
+    private lateinit var dumpManager: DumpManager
+    @Mock
+    private lateinit var packageManager: PackageManager
+    @Mock
+    private lateinit var featureFlags: FeatureFlags
 
-    private var componentName = ComponentName("pkg1", "class1")
-    private var componentName2 = ComponentName("pkg2", "class2")
+    private var componentName = ComponentName("pkg", "class1")
+    private var activityName = ComponentName("pkg", "activity")
 
     private val executor = FakeExecutor(FakeSystemClock())
 
@@ -87,9 +102,15 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(serviceInfo.componentName).thenReturn(componentName)
-        `when`(serviceInfo2.componentName).thenReturn(componentName2)
         `when`(userTracker.userId).thenReturn(user)
+        `when`(userTracker.userContext).thenReturn(context)
+        // Return disabled by default
+        `when`(packageManager.getComponentEnabledSetting(any()))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)
+        mContext.setMockPackageManager(packageManager)
+
+        // Return true by default, we'll test the false path
+        `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(true)
 
         val wrapper = object : ContextWrapper(mContext) {
             override fun createContextAsUser(user: UserHandle, flags: Int): Context {
@@ -97,7 +118,14 @@
             }
         }
 
-        controller = ControlsListingControllerImpl(wrapper, executor, { mockSL }, userTracker)
+        controller = ControlsListingControllerImpl(
+                wrapper,
+                executor,
+                { mockSL },
+                userTracker,
+                dumpManager,
+                featureFlags
+        )
         verify(mockSL).addCallback(capture(serviceListingCallbackCaptor))
     }
 
@@ -123,9 +151,16 @@
             Unit
         }
         `when`(mockServiceListing.reload()).then {
-            callback?.onServicesReloaded(listOf(serviceInfo))
+            callback?.onServicesReloaded(listOf(ServiceInfo(componentName)))
         }
-        ControlsListingControllerImpl(mContext, exec, { mockServiceListing }, userTracker)
+        ControlsListingControllerImpl(
+                mContext,
+                exec,
+                { mockServiceListing },
+                userTracker,
+                dumpManager,
+                featureFlags
+        )
     }
 
     @Test
@@ -148,7 +183,7 @@
 
     @Test
     fun testCallbackGetsList() {
-        val list = listOf(serviceInfo)
+        val list = listOf(ServiceInfo(componentName))
         controller.addCallback(mockCallback)
         controller.addCallback(mockCallbackOther)
 
@@ -188,6 +223,8 @@
 
     @Test
     fun testChangeUserSendsCorrectServiceUpdate() {
+        val serviceInfo = ServiceInfo(componentName)
+
         val list = listOf(serviceInfo)
         controller.addCallback(mockCallback)
 
@@ -223,4 +260,284 @@
         verify(mockCallback).onServicesUpdated(capture(captor))
         assertEquals(0, captor.value.size)
     }
+
+    @Test
+    fun test_nullPanelActivity() {
+        val list = listOf(ServiceInfo(componentName))
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testNoActivity_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityWithoutPermission_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        setUpQueryResult(listOf(ActivityInfo(activityName)))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityPermissionNotExported_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        setUpQueryResult(listOf(
+                ActivityInfo(activityName, permission = Manifest.permission.BIND_CONTROLS)
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDisabled_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityEnabled_correctPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertEquals(activityName, controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDefaultEnabled_correctPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = true,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertEquals(activityName, controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDefaultDisabled_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = false,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDefaultEnabled_flagDisabled_nullPanel() {
+        `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(false)
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName,
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = true,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDifferentPackage_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                ComponentName("other_package", "cls")
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = true,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    private fun ServiceInfo(
+            componentName: ComponentName,
+            panelActivityComponentName: ComponentName? = null
+    ): ServiceInfo {
+        return ServiceInfo().apply {
+            packageName = componentName.packageName
+            name = componentName.className
+            panelActivityComponentName?.let {
+                metaData = Bundle().apply {
+                    putString(
+                            ControlsProviderService.META_DATA_PANEL_ACTIVITY,
+                            it.flattenToShortString()
+                    )
+                }
+            }
+        }
+    }
+
+    private fun ActivityInfo(
+        componentName: ComponentName,
+        exported: Boolean = false,
+        enabled: Boolean = true,
+        permission: String? = null
+    ): ActivityInfo {
+        return ActivityInfo().apply {
+            packageName = componentName.packageName
+            name = componentName.className
+            this.permission = permission
+            this.exported = exported
+            this.enabled = enabled
+        }
+    }
+
+    private fun setUpQueryResult(infos: List<ActivityInfo>) {
+        `when`(
+                packageManager.queryIntentActivitiesAsUser(
+                        argThat(IntentMatcher(activityName)),
+                        argThat(FlagsMatcher(FLAGS)),
+                        eq(UserHandle.of(user))
+                )
+        ).thenReturn(infos.map {
+            ResolveInfo().apply { activityInfo = it }
+        })
+    }
+
+    private class IntentMatcher(
+            private val componentName: ComponentName
+    ) : ArgumentMatcher<Intent> {
+        override fun matches(argument: Intent?): Boolean {
+            return argument?.component == componentName
+        }
+    }
+
+    private class FlagsMatcher(
+            private val flags: Long
+    ) : ArgumentMatcher<PackageManager.ResolveInfoFlags> {
+        override fun matches(argument: PackageManager.ResolveInfoFlags?): Boolean {
+            return flags == argument?.value
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
index 318f2bc..170a70f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
@@ -20,7 +20,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
-import java.lang.IllegalStateException
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,12 +28,12 @@
 @RunWith(AndroidTestingRunner::class)
 class FakeFeatureFlagsTest : SysuiTestCase() {
 
-    private val unreleasedFlag = UnreleasedFlag(-1000)
-    private val releasedFlag = ReleasedFlag(-1001)
-    private val stringFlag = StringFlag(-1002)
-    private val resourceBooleanFlag = ResourceBooleanFlag(-1003, resourceId = -1)
-    private val resourceStringFlag = ResourceStringFlag(-1004, resourceId = -1)
-    private val sysPropBooleanFlag = SysPropBooleanFlag(-1005, name = "test")
+    private val unreleasedFlag = UnreleasedFlag(-1000, "-1000", "test")
+    private val releasedFlag = ReleasedFlag(-1001, "-1001", "test")
+    private val stringFlag = StringFlag(-1002, "-1002", "test")
+    private val resourceBooleanFlag = ResourceBooleanFlag(-1003, "-1003", "test", resourceId = -1)
+    private val resourceStringFlag = ResourceStringFlag(-1004, "-1004", "test", resourceId = -1)
+    private val sysPropBooleanFlag = SysPropBooleanFlag(-1005, "test", "test")
 
     /**
      * FakeFeatureFlags does not honor any default values. All flags which are accessed must be
@@ -47,7 +46,7 @@
             assertThat(flags.isEnabled(Flags.TEAMFOOD)).isFalse()
             fail("Expected an exception when accessing an unspecified flag.")
         } catch (ex: IllegalStateException) {
-            assertThat(ex.message).contains("TEAMFOOD")
+            assertThat(ex.message).contains("id=1")
         }
         try {
             assertThat(flags.isEnabled(unreleasedFlag)).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
index 9c22cd2..7592cc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
@@ -31,10 +31,6 @@
 import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
-import java.io.PrintWriter
-import java.io.Serializable
-import java.io.StringWriter
-import java.util.function.Consumer
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
@@ -46,8 +42,12 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import java.io.PrintWriter
+import java.io.Serializable
+import java.io.StringWriter
+import java.util.function.Consumer
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * NOTE: This test is for the version of FeatureFlagManager in src-debug, which allows overriding
@@ -57,21 +57,32 @@
 class FeatureFlagsDebugTest : SysuiTestCase() {
     private lateinit var mFeatureFlagsDebug: FeatureFlagsDebug
 
-    @Mock private lateinit var flagManager: FlagManager
-    @Mock private lateinit var mockContext: Context
-    @Mock private lateinit var secureSettings: SecureSettings
-    @Mock private lateinit var systemProperties: SystemPropertiesHelper
-    @Mock private lateinit var resources: Resources
-    @Mock private lateinit var commandRegistry: CommandRegistry
-    @Mock private lateinit var restarter: Restarter
+    @Mock
+    private lateinit var flagManager: FlagManager
+    @Mock
+    private lateinit var mockContext: Context
+    @Mock
+    private lateinit var secureSettings: SecureSettings
+    @Mock
+    private lateinit var systemProperties: SystemPropertiesHelper
+    @Mock
+    private lateinit var resources: Resources
+    @Mock
+    private lateinit var commandRegistry: CommandRegistry
+    @Mock
+    private lateinit var restarter: Restarter
     private val flagMap = mutableMapOf<Int, Flag<*>>()
     private lateinit var broadcastReceiver: BroadcastReceiver
     private lateinit var clearCacheAction: Consumer<Int>
     private val serverFlagReader = ServerFlagReaderFake()
 
     private val deviceConfig = DeviceConfigProxyFake()
-    private val teamfoodableFlagA = UnreleasedFlag(500, true)
-    private val teamfoodableFlagB = ReleasedFlag(501, true)
+    private val teamfoodableFlagA = UnreleasedFlag(
+        500, name = "a", namespace = "test", teamfood = true
+    )
+    private val teamfoodableFlagB = ReleasedFlag(
+        501, name = "b", namespace = "test", teamfood = true
+    )
 
     @Before
     fun setup() {
@@ -84,7 +95,6 @@
             secureSettings,
             systemProperties,
             resources,
-            deviceConfig,
             serverFlagReader,
             flagMap,
             restarter
@@ -92,8 +102,10 @@
         mFeatureFlagsDebug.init()
         verify(flagManager).onSettingsChangedAction = any()
         broadcastReceiver = withArgCaptor {
-            verify(mockContext).registerReceiver(capture(), any(), nullable(), nullable(),
-                any())
+            verify(mockContext).registerReceiver(
+                capture(), any(), nullable(), nullable(),
+                any()
+            )
         }
         clearCacheAction = withArgCaptor {
             verify(flagManager).clearCacheAction = capture()
@@ -107,10 +119,42 @@
         whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(4), any())).thenReturn(false)
 
-        assertThat(mFeatureFlagsDebug.isEnabled(ReleasedFlag(2))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(UnreleasedFlag(3))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(ReleasedFlag(4))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(UnreleasedFlag(5))).isFalse()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                ReleasedFlag(
+                    2,
+                    name = "2",
+                    namespace = "test"
+                )
+            )
+        ).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                UnreleasedFlag(
+                    3,
+                    name = "3",
+                    namespace = "test"
+                )
+            )
+        ).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                ReleasedFlag(
+                    4,
+                    name = "3",
+                    namespace = "test"
+                )
+            )
+        ).isFalse()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                UnreleasedFlag(
+                    5,
+                    name = "4",
+                    namespace = "test"
+                )
+            )
+        ).isFalse()
     }
 
     @Test
@@ -138,9 +182,9 @@
     @Test
     fun teamFoodFlag_Overridden() {
         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.id), any()))
-                .thenReturn(true)
+            .thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.id), any()))
-                .thenReturn(false)
+            .thenReturn(false)
         whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse()
@@ -161,17 +205,26 @@
         whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(5), any())).thenReturn(false)
 
-        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(1, 1001))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(2, 1002))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(3, 1003))).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                ResourceBooleanFlag(
+                    1,
+                    "1",
+                    "test",
+                    1001
+                )
+            )
+        ).isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(2, "2", "test", 1002))).isTrue()
+        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(3, "3", "test", 1003))).isTrue()
 
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(4, 1004))
+            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(4, "4", "test", 1004))
         }
         // Test that resource is loaded (and validated) even when the setting is set.
         //  This prevents developers from not noticing when they reference an invalid resource.
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(5, 1005))
+            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(5, "5", "test", 1005))
         }
     }
 
@@ -184,36 +237,30 @@
             return@thenAnswer it.getArgument(1)
         }
 
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(1, "a"))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(2, "b"))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(3, "c", true))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(4, "d", false))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(5, "e"))).isFalse()
-    }
-
-    @Test
-    fun readDeviceConfigBooleanFlag() {
-        val namespace = "test_namespace"
-        deviceConfig.setProperty(namespace, "a", "true", false)
-        deviceConfig.setProperty(namespace, "b", "false", false)
-        deviceConfig.setProperty(namespace, "c", null, false)
-
-        assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(1, "a", namespace)))
-            .isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(2, "b", namespace)))
-            .isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(3, "c", namespace)))
-            .isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(1, "a", "test"))).isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(2, "b", "test"))).isTrue()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(3, "c", "test", true))).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                SysPropBooleanFlag(
+                    4,
+                    "d",
+                    "test",
+                    false
+                )
+            )
+        ).isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(5, "e", "test"))).isFalse()
     }
 
     @Test
     fun readStringFlag() {
         whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("foo")
         whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("bar")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "biz"))).isEqualTo("biz")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "baz"))).isEqualTo("baz")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "buz"))).isEqualTo("foo")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(4, "buz"))).isEqualTo("bar")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "1", "test", "biz"))).isEqualTo("biz")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "2", "test", "baz"))).isEqualTo("baz")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "3", "test", "buz"))).isEqualTo("foo")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(4, "4", "test", "buz"))).isEqualTo("bar")
     }
 
     @Test
@@ -229,20 +276,47 @@
         whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("override4")
         whenever(flagManager.readFlagValue<String>(eq(6), any())).thenReturn("override6")
 
-        assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
-        assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(2, 1002))).isEqualTo("resource2")
-        assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(3, 1003))).isEqualTo("override3")
+        assertThat(
+            mFeatureFlagsDebug.getString(
+                ResourceStringFlag(
+                    1,
+                    "1",
+                    "test",
+                    1001
+                )
+            )
+        ).isEqualTo("")
+        assertThat(
+            mFeatureFlagsDebug.getString(
+                ResourceStringFlag(
+                    2,
+                    "2",
+                    "test",
+                    1002
+                )
+            )
+        ).isEqualTo("resource2")
+        assertThat(
+            mFeatureFlagsDebug.getString(
+                ResourceStringFlag(
+                    3,
+                    "3",
+                    "test",
+                    1003
+                )
+            )
+        ).isEqualTo("override3")
 
         Assert.assertThrows(NullPointerException::class.java) {
-            mFeatureFlagsDebug.getString(ResourceStringFlag(4, 1004))
+            mFeatureFlagsDebug.getString(ResourceStringFlag(4, "4", "test", 1004))
         }
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.getString(ResourceStringFlag(5, 1005))
+            mFeatureFlagsDebug.getString(ResourceStringFlag(5, "5", "test", 1005))
         }
         // Test that resource is loaded (and validated) even when the setting is set.
         //  This prevents developers from not noticing when they reference an invalid resource.
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.getString(ResourceStringFlag(6, 1005))
+            mFeatureFlagsDebug.getString(ResourceStringFlag(6, "6", "test", 1005))
         }
     }
 
@@ -250,10 +324,10 @@
     fun readIntFlag() {
         whenever(flagManager.readFlagValue<Int>(eq(3), any())).thenReturn(22)
         whenever(flagManager.readFlagValue<Int>(eq(4), any())).thenReturn(48)
-        assertThat(mFeatureFlagsDebug.getInt(IntFlag(1, 12))).isEqualTo(12)
-        assertThat(mFeatureFlagsDebug.getInt(IntFlag(2, 93))).isEqualTo(93)
-        assertThat(mFeatureFlagsDebug.getInt(IntFlag(3, 8))).isEqualTo(22)
-        assertThat(mFeatureFlagsDebug.getInt(IntFlag(4, 234))).isEqualTo(48)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(1, "1", "test", 12))).isEqualTo(12)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(2, "2", "test", 93))).isEqualTo(93)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(3, "3", "test", 8))).isEqualTo(22)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(4, "4", "test", 234))).isEqualTo(48)
     }
 
     @Test
@@ -269,26 +343,26 @@
         whenever(flagManager.readFlagValue<Int>(eq(4), any())).thenReturn(500)
         whenever(flagManager.readFlagValue<Int>(eq(5), any())).thenReturn(9519)
 
-        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(1, 1001))).isEqualTo(88)
-        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(2, 1002))).isEqualTo(61)
-        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(3, 1003))).isEqualTo(20)
+        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(1, "1", "test", 1001))).isEqualTo(88)
+        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(2, "2", "test", 1002))).isEqualTo(61)
+        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(3, "3", "test", 1003))).isEqualTo(20)
 
         Assert.assertThrows(NotFoundException::class.java) {
-            mFeatureFlagsDebug.getInt(ResourceIntFlag(4, 1004))
+            mFeatureFlagsDebug.getInt(ResourceIntFlag(4, "4", "test", 1004))
         }
         // Test that resource is loaded (and validated) even when the setting is set.
         //  This prevents developers from not noticing when they reference an invalid resource.
         Assert.assertThrows(NotFoundException::class.java) {
-            mFeatureFlagsDebug.getInt(ResourceIntFlag(5, 1005))
+            mFeatureFlagsDebug.getInt(ResourceIntFlag(5, "5", "test", 1005))
         }
     }
 
     @Test
     fun broadcastReceiver_IgnoresInvalidData() {
-        addFlag(UnreleasedFlag(1))
-        addFlag(ResourceBooleanFlag(2, 1002))
-        addFlag(StringFlag(3, "flag3"))
-        addFlag(ResourceStringFlag(4, 1004))
+        addFlag(UnreleasedFlag(1, "1", "test"))
+        addFlag(ResourceBooleanFlag(2, "2", "test", 1002))
+        addFlag(StringFlag(3, "3", "test", "flag3"))
+        addFlag(ResourceStringFlag(4, "4", "test", 1004))
 
         broadcastReceiver.onReceive(mockContext, null)
         broadcastReceiver.onReceive(mockContext, Intent())
@@ -304,7 +378,7 @@
 
     @Test
     fun intentWithId_NoValueKeyClears() {
-        addFlag(UnreleasedFlag(1))
+        addFlag(UnreleasedFlag(1, name = "1", namespace = "test"))
 
         // trying to erase an id not in the map does nothing
         broadcastReceiver.onReceive(
@@ -323,10 +397,10 @@
 
     @Test
     fun setBooleanFlag() {
-        addFlag(UnreleasedFlag(1))
-        addFlag(UnreleasedFlag(2))
-        addFlag(ResourceBooleanFlag(3, 1003))
-        addFlag(ResourceBooleanFlag(4, 1004))
+        addFlag(UnreleasedFlag(1, "1", "test"))
+        addFlag(UnreleasedFlag(2, "2", "test"))
+        addFlag(ResourceBooleanFlag(3, "3", "test", 1003))
+        addFlag(ResourceBooleanFlag(4, "4", "test", 1004))
 
         setByBroadcast(1, false)
         verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}")
@@ -343,8 +417,8 @@
 
     @Test
     fun setStringFlag() {
-        addFlag(StringFlag(1, "flag1"))
-        addFlag(ResourceStringFlag(2, 1002))
+        addFlag(StringFlag(1, "flag1", "1", "test"))
+        addFlag(ResourceStringFlag(2, "2", "test", 1002))
 
         setByBroadcast(1, "override1")
         verifyPutData(1, "{\"type\":\"string\",\"value\":\"override1\"}")
@@ -355,7 +429,7 @@
 
     @Test
     fun setFlag_ClearsCache() {
-        val flag1 = addFlag(StringFlag(1, "flag1"))
+        val flag1 = addFlag(StringFlag(1, "1", "test", "flag1"))
         whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("original")
 
         // gets the flag & cache it
@@ -377,31 +451,31 @@
 
     @Test
     fun serverSide_Overrides_MakesFalse() {
-        val flag = ReleasedFlag(100)
+        val flag = ReleasedFlag(100, "100", "test")
 
-        serverFlagReader.setFlagValue(flag.id, false)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, false)
 
         assertThat(mFeatureFlagsDebug.isEnabled(flag)).isFalse()
     }
 
     @Test
     fun serverSide_Overrides_MakesTrue() {
-        val flag = UnreleasedFlag(100)
+        val flag = UnreleasedFlag(100, name = "100", namespace = "test")
 
-        serverFlagReader.setFlagValue(flag.id, true)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
 
         assertThat(mFeatureFlagsDebug.isEnabled(flag)).isTrue()
     }
 
     @Test
     fun dumpFormat() {
-        val flag1 = ReleasedFlag(1)
-        val flag2 = ResourceBooleanFlag(2, 1002)
-        val flag3 = UnreleasedFlag(3)
-        val flag4 = StringFlag(4, "")
-        val flag5 = StringFlag(5, "flag5default")
-        val flag6 = ResourceStringFlag(6, 1006)
-        val flag7 = ResourceStringFlag(7, 1007)
+        val flag1 = ReleasedFlag(1, "1", "test")
+        val flag2 = ResourceBooleanFlag(2, "2", "test", 1002)
+        val flag3 = UnreleasedFlag(3, "3", "test")
+        val flag4 = StringFlag(4, "4", "test", "")
+        val flag5 = StringFlag(5, "5", "test", "flag5default")
+        val flag6 = ResourceStringFlag(6, "6", "test", 1006)
+        val flag7 = ResourceStringFlag(7, "7", "test", 1007)
 
         whenever(resources.getBoolean(1002)).thenReturn(true)
         whenever(resources.getString(1006)).thenReturn("resource1006")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
index b2dd60c..d5b5a4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
@@ -25,8 +25,8 @@
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow
@@ -59,7 +59,9 @@
     fun testBooleanResourceFlag() {
         val flagId = 213
         val flagResourceId = 3
-        val flag = ResourceBooleanFlag(flagId, flagResourceId)
+        val flagName = "213"
+        val flagNamespace = "test"
+        val flag = ResourceBooleanFlag(flagId, flagName, flagNamespace, flagResourceId)
         whenever(mResources.getBoolean(flagResourceId)).thenReturn(true)
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isTrue()
     }
@@ -71,57 +73,45 @@
         whenever(mResources.getString(1003)).thenReturn(null)
         whenever(mResources.getString(1004)).thenAnswer { throw NameNotFoundException() }
 
-        assertThat(mFeatureFlagsRelease.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
-        assertThat(mFeatureFlagsRelease.getString(ResourceStringFlag(2, 1002))).isEqualTo("res2")
+        assertThat(mFeatureFlagsRelease.getString(
+            ResourceStringFlag(1, "1", "test", 1001))).isEqualTo("")
+        assertThat(mFeatureFlagsRelease.getString(
+            ResourceStringFlag(2, "2", "test", 1002))).isEqualTo("res2")
 
         assertThrows(NullPointerException::class.java) {
-            mFeatureFlagsRelease.getString(ResourceStringFlag(3, 1003))
+            mFeatureFlagsRelease.getString(ResourceStringFlag(3, "3", "test", 1003))
         }
         assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsRelease.getString(ResourceStringFlag(4, 1004))
+            mFeatureFlagsRelease.getString(ResourceStringFlag(4, "4", "test", 1004))
         }
     }
 
     @Test
-    fun testReadDeviceConfigBooleanFlag() {
-        val namespace = "test_namespace"
-        deviceConfig.setProperty(namespace, "a", "true", false)
-        deviceConfig.setProperty(namespace, "b", "false", false)
-        deviceConfig.setProperty(namespace, "c", null, false)
-
-        assertThat(mFeatureFlagsRelease.isEnabled(DeviceConfigBooleanFlag(1, "a", namespace)))
-            .isTrue()
-        assertThat(mFeatureFlagsRelease.isEnabled(DeviceConfigBooleanFlag(2, "b", namespace)))
-            .isFalse()
-        assertThat(mFeatureFlagsRelease.isEnabled(DeviceConfigBooleanFlag(3, "c", namespace)))
-            .isFalse()
-    }
-
-    @Test
     fun testSysPropBooleanFlag() {
         val flagId = 213
         val flagName = "sys_prop_flag"
+        val flagNamespace = "test"
         val flagDefault = true
 
-        val flag = SysPropBooleanFlag(flagId, flagName, flagDefault)
+        val flag = SysPropBooleanFlag(flagId, flagName, flagNamespace, flagDefault)
         whenever(mSystemProperties.getBoolean(flagName, flagDefault)).thenReturn(flagDefault)
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isEqualTo(flagDefault)
     }
 
     @Test
     fun serverSide_OverridesReleased_MakesFalse() {
-        val flag = ReleasedFlag(100)
+        val flag = ReleasedFlag(100, "100", "test")
 
-        serverFlagReader.setFlagValue(flag.id, false)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, false)
 
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isFalse()
     }
 
     @Test
     fun serverSide_OverridesUnreleased_Ignored() {
-        val flag = UnreleasedFlag(100)
+        val flag = UnreleasedFlag(100, "100", "test")
 
-        serverFlagReader.setFlagValue(flag.id, true)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
 
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isFalse()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index 7355319..fea91c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
@@ -33,10 +33,10 @@
     @Mock private lateinit var featureFlags: FeatureFlagsDebug
     @Mock private lateinit var pw: PrintWriter
     private val flagMap = mutableMapOf<Int, Flag<*>>()
-    private val flagA = UnreleasedFlag(500)
-    private val flagB = ReleasedFlag(501)
-    private val stringFlag = StringFlag(502, "abracadabra")
-    private val intFlag = IntFlag(503, 12)
+    private val flagA = UnreleasedFlag(500, "500", "test")
+    private val flagB = ReleasedFlag(501, "501", "test")
+    private val stringFlag = StringFlag(502, "502", "test", "abracadabra")
+    private val intFlag = IntFlag(503, "503", "test", 12)
 
     private lateinit var cmd: FlagCommand
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
index 17324a0..fca7e96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
@@ -64,14 +64,14 @@
         verifyNoMoreInteractions(mFlagSettingsHelper)
 
         // adding the first listener registers the observer
-        mFlagManager.addListener(ReleasedFlag(1), listener1)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
         verifyNoMoreInteractions(mFlagSettingsHelper)
 
         // adding another listener does nothing
-        mFlagManager.addListener(ReleasedFlag(2), listener2)
+        mFlagManager.addListener(ReleasedFlag(2, "2", "test"), listener2)
         verifyNoMoreInteractions(mFlagSettingsHelper)
 
         // removing the original listener does nothing with second one still present
@@ -89,7 +89,7 @@
         val listener = mock<FlagListenable.Listener>()
         val clearCacheAction = mock<Consumer<Int>>()
         mFlagManager.clearCacheAction = clearCacheAction
-        mFlagManager.addListener(ReleasedFlag(1), listener)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
@@ -101,8 +101,8 @@
     fun testObserverInvokesListeners() {
         val listener1 = mock<FlagListenable.Listener>()
         val listener10 = mock<FlagListenable.Listener>()
-        mFlagManager.addListener(ReleasedFlag(1), listener1)
-        mFlagManager.addListener(ReleasedFlag(10), listener10)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener10)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
@@ -127,8 +127,8 @@
     fun testOnlySpecificFlagListenerIsInvoked() {
         val listener1 = mock<FlagListenable.Listener>()
         val listener10 = mock<FlagListenable.Listener>()
-        mFlagManager.addListener(ReleasedFlag(1), listener1)
-        mFlagManager.addListener(ReleasedFlag(10), listener10)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener10)
 
         mFlagManager.dispatchListenersAndMaybeRestart(1, null)
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
@@ -148,8 +148,8 @@
     @Test
     fun testSameListenerCanBeUsedForMultipleFlags() {
         val listener = mock<FlagListenable.Listener>()
-        mFlagManager.addListener(ReleasedFlag(1), listener)
-        mFlagManager.addListener(ReleasedFlag(10), listener)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener)
 
         mFlagManager.dispatchListenersAndMaybeRestart(1, null)
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
@@ -177,7 +177,7 @@
     @Test
     fun testListenerCanSuppressRestart() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.addListener(ReleasedFlag(1)) { event ->
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test")) { event ->
             event.requestNoRestart()
         }
         mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
@@ -188,7 +188,7 @@
     @Test
     fun testListenerOnlySuppressesRestartForOwnFlag() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.addListener(ReleasedFlag(10)) { event ->
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test")) { event ->
             event.requestNoRestart()
         }
         mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
@@ -199,10 +199,10 @@
     @Test
     fun testRestartWhenNotAllListenersRequestSuppress() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.addListener(ReleasedFlag(10)) { event ->
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test")) { event ->
             event.requestNoRestart()
         }
-        mFlagManager.addListener(ReleasedFlag(10)) {
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test")) {
             // do not request
         }
         mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.kt
deleted file mode 100644
index 2b556f1..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.kt
+++ /dev/null
@@ -1,84 +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.flags
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth
-import java.lang.StringBuilder
-import java.util.ArrayList
-import java.util.HashMap
-import org.junit.Test
-
-@SmallTest
-class FlagsTest : SysuiTestCase() {
-    @Test
-    fun testDuplicateFlagIdCheckWorks() {
-        val flags = Flags.collectFlagsInClass(DuplicateFlagContainer)
-        val duplicates = groupDuplicateFlags(flags)
-        Truth.assertWithMessage(generateAssertionMessage(duplicates))
-            .that(duplicates.size)
-            .isEqualTo(2)
-    }
-
-    @Test
-    fun testNoDuplicateFlagIds() {
-        val flags = Flags.collectFlagsInClass(Flags)
-        val duplicates = groupDuplicateFlags(flags)
-        Truth.assertWithMessage(generateAssertionMessage(duplicates))
-            .that(duplicates.size)
-            .isEqualTo(0)
-    }
-
-    private fun generateAssertionMessage(duplicates: Map<Int, List<String>>): String {
-        val stringBuilder = StringBuilder()
-        stringBuilder.append("Duplicate flag keys found: {")
-        for (id in duplicates.keys) {
-            stringBuilder
-                .append(" ")
-                .append(id)
-                .append(": [")
-                .append(java.lang.String.join(", ", duplicates[id]))
-                .append("]")
-        }
-        stringBuilder.append(" }")
-        return stringBuilder.toString()
-    }
-
-    private fun groupDuplicateFlags(flags: Map<String, Flag<*>>): Map<Int, List<String>> {
-        val grouping: MutableMap<Int, MutableList<String>> = HashMap()
-        for (flag in flags) {
-            grouping.putIfAbsent(flag.value.id, ArrayList())
-            grouping[flag.value.id]!!.add(flag.key)
-        }
-        val result: MutableMap<Int, List<String>> = HashMap()
-        for (id in grouping.keys) {
-            if (grouping[id]!!.size > 1) {
-                result[id] = grouping[id]!!
-            }
-        }
-        return result
-    }
-
-    private object DuplicateFlagContainer {
-        val A_FLAG: BooleanFlag = UnreleasedFlag(0)
-        val B_FLAG: BooleanFlag = UnreleasedFlag(0)
-        val C_FLAG = StringFlag(0)
-        val D_FLAG: BooleanFlag = UnreleasedFlag(1)
-        val E_FLAG = DoubleFlag(3)
-        val F_FLAG = DoubleFlag(3)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
index 6f5f460..1633912 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
@@ -50,7 +50,7 @@
 
     @Test
     fun testChange_alertsListener() {
-        val flag = ReleasedFlag(1)
+        val flag = ReleasedFlag(1, "1", "test")
         serverFlagReader.listenForChanges(listOf(flag), changeListener)
 
         deviceConfig.setProperty(NAMESPACE, "flag_override_1", "1", false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
deleted file mode 100644
index 4d66a16..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard
-
-import android.content.pm.PackageManager
-import android.content.pm.ProviderInfo
-import androidx.test.filters.SmallTest
-import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.SystemUIAppComponentFactoryBase
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderClient as Client
-import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
-import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
-
-    @Mock private lateinit var lockPatternUtils: LockPatternUtils
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var userTracker: UserTracker
-    @Mock private lateinit var activityStarter: ActivityStarter
-
-    private lateinit var underTest: KeyguardQuickAffordanceProvider
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest = KeyguardQuickAffordanceProvider()
-        val quickAffordanceRepository =
-            KeyguardQuickAffordanceRepository(
-                scope = CoroutineScope(IMMEDIATE),
-                backgroundDispatcher = IMMEDIATE,
-                selectionManager = KeyguardQuickAffordanceSelectionManager(),
-                configs =
-                    setOf(
-                        FakeKeyguardQuickAffordanceConfig(
-                            key = AFFORDANCE_1,
-                            pickerIconResourceId = 1,
-                        ),
-                        FakeKeyguardQuickAffordanceConfig(
-                            key = AFFORDANCE_2,
-                            pickerIconResourceId = 2,
-                        ),
-                    ),
-            )
-        underTest.interactor =
-            KeyguardQuickAffordanceInteractor(
-                keyguardInteractor =
-                    KeyguardInteractor(
-                        repository = FakeKeyguardRepository(),
-                    ),
-                registry = mock(),
-                lockPatternUtils = lockPatternUtils,
-                keyguardStateController = keyguardStateController,
-                userTracker = userTracker,
-                activityStarter = activityStarter,
-                featureFlags =
-                    FakeFeatureFlags().apply {
-                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
-                    },
-                repository = { quickAffordanceRepository },
-            )
-
-        underTest.attachInfoForTesting(
-            context,
-            ProviderInfo().apply { authority = Contract.AUTHORITY },
-        )
-        context.contentResolver.addProvider(Contract.AUTHORITY, underTest)
-        context.testablePermissions.setPermission(
-            Contract.PERMISSION,
-            PackageManager.PERMISSION_GRANTED,
-        )
-    }
-
-    @Test
-    fun `onAttachInfo - reportsContext`() {
-        val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock()
-        underTest.setContextAvailableCallback(callback)
-
-        underTest.attachInfo(context, null)
-
-        verify(callback).onContextAvailable(context)
-    }
-
-    @Test
-    fun getType() {
-        assertThat(underTest.getType(Contract.AffordanceTable.URI))
-            .isEqualTo(
-                "vnd.android.cursor.dir/vnd." +
-                    "${Contract.AUTHORITY}.${Contract.AffordanceTable.TABLE_NAME}"
-            )
-        assertThat(underTest.getType(Contract.SlotTable.URI))
-            .isEqualTo(
-                "vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}.${Contract.SlotTable.TABLE_NAME}"
-            )
-        assertThat(underTest.getType(Contract.SelectionTable.URI))
-            .isEqualTo(
-                "vnd.android.cursor.dir/vnd." +
-                    "${Contract.AUTHORITY}.${Contract.SelectionTable.TABLE_NAME}"
-            )
-    }
-
-    @Test
-    fun `insert and query selection`() =
-        runBlocking(IMMEDIATE) {
-            val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
-            val affordanceId = AFFORDANCE_2
-
-            Client.insertSelection(
-                context = context,
-                slotId = slotId,
-                affordanceId = affordanceId,
-                dispatcher = IMMEDIATE,
-            )
-
-            assertThat(
-                    Client.querySelections(
-                        context = context,
-                        dispatcher = IMMEDIATE,
-                    )
-                )
-                .isEqualTo(
-                    listOf(
-                        Client.Selection(
-                            slotId = slotId,
-                            affordanceId = affordanceId,
-                        )
-                    )
-                )
-        }
-
-    @Test
-    fun `query slots`() =
-        runBlocking(IMMEDIATE) {
-            assertThat(
-                    Client.querySlots(
-                        context = context,
-                        dispatcher = IMMEDIATE,
-                    )
-                )
-                .isEqualTo(
-                    listOf(
-                        Client.Slot(
-                            id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                            capacity = 1,
-                        ),
-                        Client.Slot(
-                            id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                            capacity = 1,
-                        ),
-                    )
-                )
-        }
-
-    @Test
-    fun `query affordances`() =
-        runBlocking(IMMEDIATE) {
-            assertThat(
-                    Client.queryAffordances(
-                        context = context,
-                        dispatcher = IMMEDIATE,
-                    )
-                )
-                .isEqualTo(
-                    listOf(
-                        Client.Affordance(
-                            id = AFFORDANCE_1,
-                            name = AFFORDANCE_1,
-                            iconResourceId = 1,
-                        ),
-                        Client.Affordance(
-                            id = AFFORDANCE_2,
-                            name = AFFORDANCE_2,
-                            iconResourceId = 2,
-                        ),
-                    )
-                )
-        }
-
-    @Test
-    fun `delete and query selection`() =
-        runBlocking(IMMEDIATE) {
-            Client.insertSelection(
-                context = context,
-                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                affordanceId = AFFORDANCE_1,
-                dispatcher = IMMEDIATE,
-            )
-            Client.insertSelection(
-                context = context,
-                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                affordanceId = AFFORDANCE_2,
-                dispatcher = IMMEDIATE,
-            )
-
-            Client.deleteSelection(
-                context = context,
-                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                affordanceId = AFFORDANCE_2,
-                dispatcher = IMMEDIATE,
-            )
-
-            assertThat(
-                    Client.querySelections(
-                        context = context,
-                        dispatcher = IMMEDIATE,
-                    )
-                )
-                .isEqualTo(
-                    listOf(
-                        Client.Selection(
-                            slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                            affordanceId = AFFORDANCE_1,
-                        )
-                    )
-                )
-        }
-
-    @Test
-    fun `delete all selections in a slot`() =
-        runBlocking(IMMEDIATE) {
-            Client.insertSelection(
-                context = context,
-                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                affordanceId = AFFORDANCE_1,
-                dispatcher = IMMEDIATE,
-            )
-            Client.insertSelection(
-                context = context,
-                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                affordanceId = AFFORDANCE_2,
-                dispatcher = IMMEDIATE,
-            )
-
-            Client.deleteAllSelections(
-                context = context,
-                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                dispatcher = IMMEDIATE,
-            )
-
-            assertThat(
-                    Client.querySelections(
-                        context = context,
-                        dispatcher = IMMEDIATE,
-                    )
-                )
-                .isEqualTo(
-                    listOf(
-                        Client.Selection(
-                            slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                            affordanceId = AFFORDANCE_1,
-                        )
-                    )
-                )
-        }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-        private const val AFFORDANCE_1 = "affordance_1"
-        private const val AFFORDANCE_2 = "affordance_2"
-    }
-}
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 369e5d8..1f71e3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -94,6 +94,7 @@
 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;
@@ -307,7 +308,7 @@
         MockitoAnnotations.initMocks(this);
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
-                mInteractionJankMonitor);
+                mInteractionJankMonitor, mShadeExpansionStateManager);
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -378,7 +379,7 @@
                         mDumpManager,
                         mock(HeadsUpManagerPhone.class),
                         new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
-                                mInteractionJankMonitor),
+                                mInteractionJankMonitor, mShadeExpansionStateManager),
                         mKeyguardBypassController,
                         mDozeParameters,
                         mScreenOffAnimationController);
@@ -491,6 +492,7 @@
                 mUnlockedScreenOffAnimationController,
                 mShadeTransitionController,
                 systemClock,
+                mock(CameraGestureHelper.class),
                 mKeyguardBottomAreaViewModel,
                 mKeyguardBottomAreaInteractor,
                 mDumpManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 95cf9d6..d7d17b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -239,9 +239,9 @@
 
     @Test
     public void setPanelExpanded_notFocusable_altFocusable_whenPanelIsOpen() {
-        mNotificationShadeWindowController.setPanelExpanded(true);
+        mNotificationShadeWindowController.onShadeExpansionFullyChanged(true);
         clearInvocations(mWindowManager);
-        mNotificationShadeWindowController.setPanelExpanded(true);
+        mNotificationShadeWindowController.onShadeExpansionFullyChanged(true);
         verifyNoMoreInteractions(mWindowManager);
         mNotificationShadeWindowController.setNotificationShadeFocusable(true);
 
@@ -313,7 +313,7 @@
         verifyNoMoreInteractions(mWindowManager);
 
         clearInvocations(mWindowManager);
-        mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
+        mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> {
             mNotificationShadeWindowController.setForceDozeBrightness(false);
             verify(mWindowManager, never()).updateViewLayout(any(), any());
         });
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/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 1d8e5de..5124eb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -48,6 +49,7 @@
 
     @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
     @Mock private lateinit var mockDarkAnimator: ObjectAnimator
+    @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
 
     private lateinit var controller: StatusBarStateControllerImpl
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -62,7 +64,7 @@
         controller = object : StatusBarStateControllerImpl(
             uiEventLogger,
             mock(DumpManager::class.java),
-            interactionJankMonitor
+            interactionJankMonitor, shadeExpansionStateManager
         ) {
             override fun createDarkAnimator(): ObjectAnimator { return mockDarkAnimator }
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
index b2dc842..7117c23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
@@ -41,6 +41,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
@@ -88,6 +89,7 @@
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private NotificationListener mListener;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private NotificationEntry mEntry;
     private TestableNotificationLogger mLogger;
@@ -118,6 +120,7 @@
                 mVisibilityProvider,
                 mNotifPipeline,
                 mock(StatusBarStateControllerImpl.class),
+                mShadeExpansionStateManager,
                 mBarService,
                 mExpansionStateLogger
         );
@@ -152,7 +155,7 @@
 
         when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
         when(mActiveNotifEntries.getValue()).thenReturn(Lists.newArrayList(mEntry));
-        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
+        mLogger.onChildLocationsChanged();
         TestableLooper.get(this).processAllMessages();
         mUiBgExecutor.runAllReady();
 
@@ -162,7 +165,7 @@
 
         // |mEntry| won't change visibility, so it shouldn't be reported again:
         Mockito.reset(mBarService);
-        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
+        mLogger.onChildLocationsChanged();
         TestableLooper.get(this).processAllMessages();
         mUiBgExecutor.runAllReady();
 
@@ -174,7 +177,7 @@
             throws Exception {
         when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
         when(mActiveNotifEntries.getValue()).thenReturn(Lists.newArrayList(mEntry));
-        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
+        mLogger.onChildLocationsChanged();
         TestableLooper.get(this).processAllMessages();
         mUiBgExecutor.runAllReady();
         Mockito.reset(mBarService);
@@ -189,13 +192,13 @@
     }
 
     private void setStateAsleep() {
-        mLogger.onPanelExpandedChanged(true);
+        mLogger.onShadeExpansionFullyChanged(true);
         mLogger.onDozingChanged(true);
         mLogger.onStateChanged(StatusBarState.KEYGUARD);
     }
 
     private void setStateAwake() {
-        mLogger.onPanelExpandedChanged(false);
+        mLogger.onShadeExpansionFullyChanged(false);
         mLogger.onDozingChanged(false);
         mLogger.onStateChanged(StatusBarState.SHADE);
     }
@@ -221,7 +224,7 @@
         when(mActiveNotifEntries.getValue()).thenReturn(Lists.newArrayList(mEntry));
         setStateAwake();
         // Now expand panel
-        mLogger.onPanelExpandedChanged(true);
+        mLogger.onShadeExpansionFullyChanged(true);
         assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
         assertFalse(mNotificationPanelLoggerFake.get(0).isLockscreen);
         assertEquals(1, mNotificationPanelLoggerFake.get(0).list.notifications.length);
@@ -263,6 +266,7 @@
                 NotificationVisibilityProvider visibilityProvider,
                 NotifPipeline notifPipeline,
                 StatusBarStateControllerImpl statusBarStateController,
+                ShadeExpansionStateManager shadeExpansionStateManager,
                 IStatusBarService barService,
                 ExpansionStateLogger expansionStateLogger) {
             super(
@@ -272,6 +276,7 @@
                     visibilityProvider,
                     notifPipeline,
                     statusBarStateController,
+                    shadeExpansionStateManager,
                     expansionStateLogger,
                     mNotificationPanelLoggerFake
             );
@@ -280,9 +285,5 @@
             // Make this on the current thread so we can wait for it during tests.
             mHandler = Handler.createAsync(Looper.myLooper());
         }
-
-        OnChildLocationsChangedListener getChildLocationsChangedListenerForTest() {
-            return mNotificationLocationsChangedListener;
-        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 112e759..2b189b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -56,6 +56,7 @@
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -151,7 +152,8 @@
                 mock(ConfigurationControllerImpl.class),
                 new Handler(mTestLooper.getLooper()),
                 mock(AccessibilityManagerWrapper.class),
-                mock(UiEventLogger.class)
+                mock(UiEventLogger.class),
+                mock(ShadeExpansionStateManager.class)
         );
         mIconManager = new IconManager(
                 mock(CommonNotifCollection.class),
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 d5bfe1f..5ebaf69 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,7 +41,6 @@
 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;
@@ -61,8 +60,6 @@
 
 import java.util.Optional;
 
-import dagger.Lazy;
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
@@ -87,7 +84,6 @@
     @Mock private Vibrator mVibrator;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private SystemBarAttributesListener mSystemBarAttributesListener;
-    @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -119,8 +115,7 @@
                 Optional.of(mVibrator),
                 new DisableFlagsLogger(),
                 DEFAULT_DISPLAY,
-                mSystemBarAttributesListener,
-                mCameraLauncherLazy);
+                mSystemBarAttributesListener);
 
         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 5818e57..5ad1431 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,7 +112,6 @@
 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;
@@ -221,6 +220,7 @@
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
     @Mock private StatusBarStateControllerImpl mStatusBarStateController;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private BatteryController mBatteryController;
     @Mock private DeviceProvisionedController mDeviceProvisionedController;
     @Mock private StatusBarNotificationPresenter mNotificationPresenter;
@@ -288,8 +288,6 @@
     @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.
@@ -342,6 +340,7 @@
                 mVisibilityProvider,
                 mock(NotifPipeline.class),
                 mStatusBarStateController,
+                mShadeExpansionStateManager,
                 mExpansionStateLogger,
                 new NotificationPanelLoggerFake()
         );
@@ -381,7 +380,6 @@
 
         when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper);
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
-        when(mCameraLauncherLazy.get()).thenReturn(mCameraLauncher);
 
         when(mStatusBarComponentFactory.create()).thenReturn(mCentralSurfacesComponent);
         when(mCentralSurfacesComponent.getNotificationShadeWindowViewController()).thenReturn(
@@ -483,9 +481,7 @@
                 mActivityLaunchAnimator,
                 mJankMonitor,
                 mDeviceStateManager,
-                mWiredChargingRippleController,
-                mDreamManager,
-                mCameraLauncherLazy) {
+                mWiredChargingRippleController, mDreamManager) {
             @Override
             protected ViewRootImpl getViewRootImpl() {
                 return mViewRootImpl;
@@ -897,7 +893,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
-        when(mCameraLauncher.isLaunchingAffordance()).thenReturn(true);
+        when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(true);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
     }
@@ -933,7 +929,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
-        when(mCameraLauncher.isLaunchingAffordance()).thenReturn(false);
+        when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(false);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD));
     }
@@ -1031,7 +1027,7 @@
     @Test
     public void collapseShade_callsAnimateCollapsePanels_whenExpanded() {
         // GIVEN the shade is expanded
-        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.onShadeExpansionFullyChanged(true);
         mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
 
         // WHEN collapseShade is called
@@ -1044,7 +1040,7 @@
     @Test
     public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() {
         // GIVEN the shade is collapsed
-        mCentralSurfaces.setPanelExpanded(false);
+        mCentralSurfaces.onShadeExpansionFullyChanged(false);
         mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
 
         // WHEN collapseShade is called
@@ -1057,7 +1053,7 @@
     @Test
     public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() {
         // GIVEN the shade is expanded & flag enabled
-        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.onShadeExpansionFullyChanged(true);
         mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
         mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false);
 
@@ -1071,7 +1067,7 @@
     @Test
     public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() {
         // GIVEN the shade is expanded & flag enabled
-        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.onShadeExpansionFullyChanged(true);
         mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
         mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index e252401..780e0c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -31,6 +31,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.AlertingNotificationManagerTest;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -67,6 +68,7 @@
     @Mock private KeyguardBypassController mBypassController;
     @Mock private ConfigurationControllerImpl mConfigurationController;
     @Mock private AccessibilityManagerWrapper mAccessibilityManagerWrapper;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private UiEventLogger mUiEventLogger;
     private boolean mLivesPastNormalTime;
 
@@ -81,7 +83,8 @@
                 ConfigurationController configurationController,
                 Handler handler,
                 AccessibilityManagerWrapper accessibilityManagerWrapper,
-                UiEventLogger uiEventLogger
+                UiEventLogger uiEventLogger,
+                ShadeExpansionStateManager shadeExpansionStateManager
         ) {
             super(
                     context,
@@ -93,7 +96,8 @@
                     configurationController,
                     handler,
                     accessibilityManagerWrapper,
-                    uiEventLogger
+                    uiEventLogger,
+                    shadeExpansionStateManager
             );
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
@@ -125,7 +129,8 @@
                 mConfigurationController,
                 mTestHandler,
                 mAccessibilityManagerWrapper,
-                mUiEventLogger
+                mUiEventLogger,
+                mShadeExpansionStateManager
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 9dea48e..8572478 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -123,27 +123,37 @@
     }
 
     @Test
-    fun displayView_screenOff_wakeLockAcquired() {
+    fun displayView_wakeLockAcquired() {
         underTest.displayView(getState())
 
         assertThat(fakeWakeLock.isHeld).isTrue()
     }
 
     @Test
-    fun displayView_screenAlreadyOn_wakeLockNotAcquired() {
+    fun displayView_screenAlreadyOn_wakeLockAcquired() {
         whenever(powerManager.isScreenOn).thenReturn(true)
 
         underTest.displayView(getState())
 
+        assertThat(fakeWakeLock.isHeld).isTrue()
+    }
+
+    @Test
+    fun displayView_wakeLockCanBeReleasedAfterTimeOut() {
+        underTest.displayView(getState())
+        assertThat(fakeWakeLock.isHeld).isTrue()
+
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
         assertThat(fakeWakeLock.isHeld).isFalse()
     }
 
     @Test
-    fun displayView_screenOff_wakeLockCanBeReleasedAfterTimeOut() {
+    fun displayView_removeView_wakeLockCanBeReleased() {
         underTest.displayView(getState())
         assertThat(fakeWakeLock.isHeld).isTrue()
 
-        fakeClock.advanceTime(TIMEOUT_MS + 1)
+        underTest.removeView("test reason")
 
         assertThat(fakeWakeLock.isHeld).isFalse()
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index a35427f..6c82cef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -27,7 +27,7 @@
     private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>()
 
     init {
-        Flags.flagFields.forEach { entry: Map.Entry<String, Flag<*>> ->
+        FlagsFactory.knownFlags.forEach { entry: Map.Entry<String, Flag<*>> ->
             knownFlagNames[entry.value.id] = entry.key
         }
     }
@@ -87,8 +87,6 @@
 
     override fun isEnabled(flag: ResourceBooleanFlag): Boolean = requireBooleanValue(flag.id)
 
-    override fun isEnabled(flag: DeviceConfigBooleanFlag): Boolean = requireBooleanValue(flag.id)
-
     override fun isEnabled(flag: SysPropBooleanFlag): Boolean = requireBooleanValue(flag.id)
 
     override fun getString(flag: StringFlag): String = requireStringValue(flag.id)
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index e40f001..933d259 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -473,18 +473,6 @@
     }
 
     /**
-     * The {@link UserManager#isUserVisible() user visibility} changed.
-     *
-     * <p>This callback is called before the user starts or is switched to (or after it stops), when
-     * its visibility changed because of that action.
-     *
-     * @hide
-     */
-    // NOTE: change visible to int if this method becomes a @SystemApi
-    public void onUserVisibilityChanged(@NonNull TargetUser user, boolean visible) {
-    }
-
-    /**
      * Called when an existing user is stopping, for system services to finalize any per-user
      * state they maintain for running users.  This is called prior to sending the SHUTDOWN
      * broadcast to the user; it is a good place to stop making use of any resources of that
diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java
index 953e850..c3cd135 100644
--- a/services/core/java/com/android/server/SystemServiceManager.java
+++ b/services/core/java/com/android/server/SystemServiceManager.java
@@ -82,10 +82,6 @@
     private static final String USER_STOPPING = "Stop"; // Logged as onUserStopping()
     private static final String USER_STOPPED = "Cleanup"; // Logged as onUserStopped()
     private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onUserCompletedEvent()
-    private static final String USER_VISIBLE = "Visible"; // Logged on onUserVisible() and
-                                                          // onUserStarting() (when visible is true)
-    private static final String USER_INVISIBLE = "Invisible"; // Logged on onUserStopping()
-                                                              // (when visibilityChanged is true)
 
     // The default number of threads to use if lifecycle thread pool is enabled.
     private static final int DEFAULT_MAX_USER_POOL_THREADS = 3;
@@ -354,58 +350,17 @@
     /**
      * Starts the given user.
      */
-    public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId,
-            boolean visible) {
-        EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId, visible ? 1 : 0);
+    public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) {
+        EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId);
 
         final TargetUser targetUser = newTargetUser(userId);
         synchronized (mTargetUsers) {
             mTargetUsers.put(userId, targetUser);
         }
-
-        if (visible) {
-            // Must send the user visiiblity change first, for 2 reasons:
-            // 1. Automotive need to update the user-zone mapping ASAP and it's one of the few
-            // services listening to this event (OTOH, there  are manyy listeners to USER_STARTING
-            // and some can take a while to process it)
-            // 2. When a user is switched from bg to fg, the onUserVisibilityChanged() callback is
-            // called onUserSwitching(), so calling it before onUserStarting() make it more
-            // consistent with that
-            EventLog.writeEvent(EventLogTags.SSM_USER_VISIBILITY_CHANGED, userId, /* visible= */ 1);
-            onUser(t, USER_VISIBLE, /* prevUser= */ null, targetUser);
-        }
         onUser(t, USER_STARTING, /* prevUser= */ null, targetUser);
     }
 
     /**
-     * Updates the user visibility.
-     *
-     * <p><b>NOTE: </b>this method should only be called when a user that is already running become
-     * visible; if the user is starting visible, callers should call
-     * {@link #onUserStarting(TimingsTraceAndSlog, int, boolean)} instead.
-     */
-    public void onUserVisible(@UserIdInt int userId) {
-        EventLog.writeEvent(EventLogTags.SSM_USER_VISIBILITY_CHANGED, userId, /* visible= */ 1);
-        onUser(USER_VISIBLE, userId);
-    }
-
-    /**
-     * Updates the visibility of the system user.
-     *
-     * <p>Since the system user never stops, this method must be called when it's switched from / to
-     * foreground.
-     */
-    public void onSystemUserVisibilityChanged(boolean visible) {
-        int userId = UserHandle.USER_SYSTEM;
-        EventLog.writeEvent(EventLogTags.SSM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0);
-        if (visible) {
-            onUser(USER_VISIBLE, userId);
-        } else {
-            onUser(USER_INVISIBLE, userId);
-        }
-    }
-
-    /**
      * Unlocks the given user.
      */
     public void onUserUnlocking(@UserIdInt int userId) {
@@ -452,12 +407,9 @@
     /**
      * Stops the given user.
      */
-    public void onUserStopping(@UserIdInt int userId, boolean visibilityChanged) {
-        EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId, visibilityChanged ? 1 : 0);
+    public void onUserStopping(@UserIdInt int userId) {
+        EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId);
         onUser(USER_STOPPING, userId);
-        if (visibilityChanged) {
-            onUser(USER_INVISIBLE, userId);
-        }
     }
 
     /**
@@ -580,12 +532,6 @@
                         threadPool.submit(getOnUserCompletedEventRunnable(
                                 t, service, serviceName, curUser, completedEventType));
                         break;
-                    case USER_VISIBLE:
-                        service.onUserVisibilityChanged(curUser, /* visible= */ true);
-                        break;
-                    case USER_INVISIBLE:
-                        service.onUserVisibilityChanged(curUser, /* visible= */ false);
-                        break;
                     default:
                         throw new IllegalArgumentException(onWhat + " what?");
                 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2761a86..af6eaf4 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19050,6 +19050,10 @@
         return mOomAdjuster.mCachedAppOptimizer.useFreezer();
     }
 
+    public boolean isAppFreezerExemptInstPkg() {
+        return mOomAdjuster.mCachedAppOptimizer.freezerExemptInstPkg();
+    }
+
     /**
      * Resets the state of the {@link com.android.server.am.AppErrors} instance.
      * This is intended for testing within the CTS only and is protected by
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 9d96008..dfac82c 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -141,7 +141,8 @@
      */
     public int MAX_RUNNING_PROCESS_QUEUES = DEFAULT_MAX_RUNNING_PROCESS_QUEUES;
     private static final String KEY_MAX_RUNNING_PROCESS_QUEUES = "bcast_max_running_process_queues";
-    private static final int DEFAULT_MAX_RUNNING_PROCESS_QUEUES = 4;
+    private static final int DEFAULT_MAX_RUNNING_PROCESS_QUEUES =
+            ActivityManager.isLowRamDeviceStatic() ? 2 : 4;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of active broadcasts
@@ -150,7 +151,8 @@
      */
     public int MAX_RUNNING_ACTIVE_BROADCASTS = DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS;
     private static final String KEY_MAX_RUNNING_ACTIVE_BROADCASTS = "bcast_max_running_active_broadcasts";
-    private static final int DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS = 16;
+    private static final int DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS =
+            ActivityManager.isLowRamDeviceStatic() ? 8 : 16;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of pending
@@ -159,7 +161,8 @@
      */
     public int MAX_PENDING_BROADCASTS = DEFAULT_MAX_PENDING_BROADCASTS;
     private static final String KEY_MAX_PENDING_BROADCASTS = "bcast_max_pending_broadcasts";
-    private static final int DEFAULT_MAX_PENDING_BROADCASTS = 256;
+    private static final int DEFAULT_MAX_PENDING_BROADCASTS =
+            ActivityManager.isLowRamDeviceStatic() ? 128 : 256;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to normal
@@ -167,7 +170,7 @@
      */
     public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS;
     private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis";
-    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 0;
+    private static final long DEFAULT_DELAY_NORMAL_MILLIS = +500;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts
@@ -175,7 +178,7 @@
      */
     public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS;
     private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis";
-    private static final long DEFAULT_DELAY_CACHED_MILLIS = +30_000;
+    private static final long DEFAULT_DELAY_CACHED_MILLIS = +120_000;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to urgent
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index c3839a9..3dee262 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -37,6 +37,7 @@
 import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER;
 import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_START_RECEIVER;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UptimeMillisLong;
@@ -239,7 +240,11 @@
             }
             case MSG_DELIVERY_TIMEOUT_SOFT: {
                 synchronized (mService) {
-                    deliveryTimeoutSoftLocked((BroadcastProcessQueue) msg.obj);
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1;
+                    final long originalTimeout = args.argl1;
+                    args.recycle();
+                    deliveryTimeoutSoftLocked(queue, originalTimeout);
                 }
                 return true;
             }
@@ -599,6 +604,7 @@
         // If nothing to dispatch, send any pending result immediately
         if (r.receivers.isEmpty()) {
             scheduleResultTo(r);
+            notifyFinishBroadcast(r);
         }
 
         traceEnd(cookie);
@@ -746,8 +752,11 @@
             queue.lastCpuDelayTime = queue.app.getCpuDelayTime();
 
             final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT;
+            final SomeArgs args = SomeArgs.obtain();
+            args.arg1 = queue;
+            args.argl1 = timeout;
             mLocalHandler.sendMessageDelayed(
-                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_SOFT, queue), timeout);
+                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_SOFT, args), timeout);
         }
 
         if (r.allowBackgroundActivityStarts) {
@@ -834,15 +843,16 @@
         r.resultTo = null;
     }
 
-    private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue) {
+    private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue,
+            @DurationMillisLong long originalTimeout) {
         if (queue.app != null) {
             // Instead of immediately triggering an ANR, extend the timeout by
             // the amount of time the process was runnable-but-waiting; we're
             // only willing to do this once before triggering an hard ANR
             final long cpuDelayTime = queue.app.getCpuDelayTime() - queue.lastCpuDelayTime;
-            final long timeout = MathUtils.constrain(cpuDelayTime, 0, mConstants.TIMEOUT);
+            final long hardTimeout = MathUtils.constrain(cpuDelayTime, 0, originalTimeout);
             mLocalHandler.sendMessageDelayed(
-                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), timeout);
+                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), hardTimeout);
         } else {
             deliveryTimeoutHardLocked(queue);
         }
@@ -1402,30 +1412,34 @@
 
         final boolean recordFinished = (r.terminalCount == r.receivers.size());
         if (recordFinished) {
-            mService.notifyBroadcastFinishedLocked(r);
-            mHistory.addBroadcastToHistoryLocked(r);
+            notifyFinishBroadcast(r);
+        }
+    }
 
-            r.finishTime = SystemClock.uptimeMillis();
-            r.nextReceiver = r.receivers.size();
-            BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
+    private void notifyFinishBroadcast(@NonNull BroadcastRecord r) {
+        mService.notifyBroadcastFinishedLocked(r);
+        mHistory.addBroadcastToHistoryLocked(r);
 
-            if (r.intent.getComponent() == null && r.intent.getPackage() == null
-                    && (r.intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
-                int manifestCount = 0;
-                int manifestSkipCount = 0;
-                for (int i = 0; i < r.receivers.size(); i++) {
-                    if (r.receivers.get(i) instanceof ResolveInfo) {
-                        manifestCount++;
-                        if (r.delivery[i] == BroadcastRecord.DELIVERY_SKIPPED) {
-                            manifestSkipCount++;
-                        }
+        r.finishTime = SystemClock.uptimeMillis();
+        r.nextReceiver = r.receivers.size();
+        BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
+
+        if (r.intent.getComponent() == null && r.intent.getPackage() == null
+                && (r.intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
+            int manifestCount = 0;
+            int manifestSkipCount = 0;
+            for (int i = 0; i < r.receivers.size(); i++) {
+                if (r.receivers.get(i) instanceof ResolveInfo) {
+                    manifestCount++;
+                    if (r.delivery[i] == BroadcastRecord.DELIVERY_SKIPPED) {
+                        manifestSkipCount++;
                     }
                 }
-
-                final long dispatchTime = SystemClock.uptimeMillis() - r.enqueueTime;
-                mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
-                        manifestCount, manifestSkipCount, dispatchTime);
             }
+
+            final long dispatchTime = SystemClock.uptimeMillis() - r.enqueueTime;
+            mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
+                    manifestCount, manifestSkipCount, dispatchTime);
         }
     }
 
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index cbf0aae..2d7b0dc 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -89,6 +89,8 @@
             "compact_proc_state_throttle";
     @VisibleForTesting static final String KEY_FREEZER_DEBOUNCE_TIMEOUT =
             "freeze_debounce_timeout";
+    @VisibleForTesting static final String KEY_FREEZER_EXEMPT_INST_PKG =
+            "freeze_exempt_inst_pkg";
 
     // RSS Indices
     private static final int RSS_TOTAL_INDEX = 0;
@@ -137,6 +139,7 @@
     @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE =
             String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER);
     @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L;
+    @VisibleForTesting static final Boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = true;
 
     @VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
                 Settings.Global.CACHED_APPS_FREEZER_ENABLED);
@@ -277,6 +280,8 @@
                         for (String name : properties.getKeyset()) {
                             if (KEY_FREEZER_DEBOUNCE_TIMEOUT.equals(name)) {
                                 updateFreezerDebounceTimeout();
+                            } else if (KEY_FREEZER_EXEMPT_INST_PKG.equals(name)) {
+                                updateFreezerExemptInstPkg();
                             }
                         }
                     }
@@ -357,6 +362,7 @@
     private boolean mFreezerOverride = false;
 
     @VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
+    @VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
 
     // Maps process ID to last compaction statistics for processes that we've fully compacted. Used
     // when evaluating throttles that we only consider for "full" compaction, so we don't store
@@ -566,6 +572,15 @@
         }
     }
 
+    /**
+     * Returns whether freezer exempts INSTALL_PACKAGES.
+     */
+    public boolean freezerExemptInstPkg() {
+        synchronized (mPhenotypeFlagLock) {
+            return mUseFreezer && mFreezerExemptInstPkg;
+        }
+    }
+
     @GuardedBy("mProcLock")
     void dump(PrintWriter pw) {
         pw.println("CachedAppOptimizer settings");
@@ -647,6 +662,7 @@
             pw.println("  " + KEY_USE_FREEZER + "=" + mUseFreezer);
             pw.println("  " + KEY_FREEZER_STATSD_SAMPLE_RATE + "=" + mFreezerStatsdSampleRate);
             pw.println("  " + KEY_FREEZER_DEBOUNCE_TIMEOUT + "=" + mFreezerDebounceTimeout);
+            pw.println("  " + KEY_FREEZER_EXEMPT_INST_PKG + "=" + mFreezerExemptInstPkg);
             synchronized (mProcLock) {
                 int size = mFrozenProcesses.size();
                 pw.println("  Apps frozen: " + size);
@@ -1007,6 +1023,7 @@
                     KEY_USE_FREEZER, DEFAULT_USE_FREEZER)) {
             mUseFreezer = isFreezerSupported();
             updateFreezerDebounceTimeout();
+            updateFreezerExemptInstPkg();
         } else {
             mUseFreezer = false;
         }
@@ -1194,6 +1211,15 @@
         if (mFreezerDebounceTimeout < 0) {
             mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
         }
+        Slog.d(TAG_AM, "Freezer timeout set to " + mFreezerDebounceTimeout);
+    }
+
+    @GuardedBy("mPhenotypeFlagLock")
+    private void updateFreezerExemptInstPkg() {
+        mFreezerExemptInstPkg = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_EXEMPT_INST_PKG, DEFAULT_FREEZER_EXEMPT_INST_PKG);
+        Slog.d(TAG_AM, "Freezer exemption set to " + mFreezerExemptInstPkg);
     }
 
     private boolean parseProcStateThrottle(String procStateThrottleString) {
diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags
index 60e6754..ea3c8dc 100644
--- a/services/core/java/com/android/server/am/EventLogTags.logtags
+++ b/services/core/java/com/android/server/am/EventLogTags.logtags
@@ -107,21 +107,21 @@
 30079 uc_dispatch_user_switch (oldUserId|1|5),(newUserId|1|5)
 30080 uc_continue_user_switch (oldUserId|1|5),(newUserId|1|5)
 30081 uc_send_user_broadcast (userId|1|5),(IntentAction|3)
+
 # Tags below are used by SystemServiceManager - although it's technically part of am, these are
 # also user switch events and useful to be analyzed together with events above.
-30082 ssm_user_starting (userId|1|5),(visible|1)
+30082 ssm_user_starting (userId|1|5)
 30083 ssm_user_switching (oldUserId|1|5),(newUserId|1|5)
 30084 ssm_user_unlocking (userId|1|5)
 30085 ssm_user_unlocked (userId|1|5)
-30086 ssm_user_stopping (userId|1|5),(visibilityChanged|1)
+30086 ssm_user_stopping (userId|1|5)
 30087 ssm_user_stopped (userId|1|5)
 30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5)
-30089 ssm_user_visibility_changed (userId|1|5),(visible|1)
+
+# Similarly, tags below are used by UserManagerService
+30091 um_user_visibility_changed (userId|1|5),(visible|1)
 
 # Foreground service start/stop events.
 30100 am_foreground_service_start (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
 30101 am_foreground_service_denied (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
 30102 am_foreground_service_stop (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
-
-
-
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 42bfc4c..ecea96e 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -1693,7 +1693,8 @@
                             app.info.packageName);
                     externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid,
                             app.info.packageName);
-                    if (pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,
+                    if (mService.isAppFreezerExemptInstPkg()
+                            && pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,
                             app.info.packageName, userId)
                             == PackageManager.PERMISSION_GRANTED) {
                         Slog.i(TAG, app.info.packageName + " is exempt from freezer");
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 8049a3a..9213327 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -177,9 +177,7 @@
     static final int START_USER_SWITCH_FG_MSG = 120;
     static final int COMPLETE_USER_SWITCH_MSG = 130;
     static final int USER_COMPLETED_EVENT_MSG = 140;
-    static final int USER_VISIBLE_MSG = 150;
-
-    private static final int NO_ARG2 = 0;
+    static final int USER_VISIBILITY_CHANGED_MSG = 150;
 
     // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if
     // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not
@@ -439,10 +437,13 @@
     /** @see #getLastUserUnlockingUptime */
     private volatile long mLastUserUnlockingUptime = 0;
 
+    // TODO(b/244333150) remove this array and let UserVisibilityMediator call the listeners
+    // directly, as that class should be responsible for all user visibility logic (for example,
+    // when the foreground user is switched out, its profiles also become invisible)
     /**
      * List of visible users (as defined by {@link UserManager#isUserVisible()}).
      *
-     * <p>It's only used to call {@link SystemServiceManager} when the visibility is changed upon
+     * <p>It's only used to call {@link UserManagerInternal} when the visibility is changed upon
      * the user starting or stopping.
      *
      * <p>Note: only the key is used, not the value.
@@ -1096,10 +1097,7 @@
             synchronized (mLock) {
                 visibleBefore = mVisibleUsers.get(userId);
                 if (visibleBefore) {
-                    if (DEBUG_MU) {
-                        Slogf.d(TAG, "Removing %d from mVisibleUsers", userId);
-                    }
-                    mVisibleUsers.delete(userId);
+                    deleteVisibleUserLocked(userId);
                     visibilityChanged = true;
                 } else {
                     visibilityChanged = false;
@@ -1148,6 +1146,20 @@
         }
     }
 
+    private void addVisibleUserLocked(@UserIdInt int userId) {
+        if (DEBUG_MU) {
+            Slogf.d(TAG, "adding %d to mVisibleUsers", userId);
+        }
+        mVisibleUsers.put(userId, true);
+    }
+
+    private void deleteVisibleUserLocked(@UserIdInt int userId) {
+        if (DEBUG_MU) {
+            Slogf.d(TAG, "deleting %d from mVisibleUsers", userId);
+        }
+        mVisibleUsers.delete(userId);
+    }
+
     private void finishUserStopping(final int userId, final UserState uss,
             final boolean allowDelayedLocking, final boolean visibilityChanged) {
         EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId);
@@ -1166,7 +1178,10 @@
         mInjector.batteryStatsServiceNoteEvent(
                 BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH,
                 Integer.toString(userId), userId);
-        mInjector.getSystemServiceManager().onUserStopping(userId, visibilityChanged);
+        mInjector.getSystemServiceManager().onUserStopping(userId);
+        if (visibilityChanged) {
+            mInjector.onUserVisibilityChanged(userId, /* visible= */ false);
+        }
 
         Runnable finishUserStoppedAsync = () ->
                 mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking));
@@ -1635,7 +1650,12 @@
                 return false;
             }
 
-            mInjector.getUserManagerInternal().assignUserToDisplay(userId, displayId);
+            if (!userInfo.preCreated) {
+                // TODO(b/244644281): UMI should return whether the user is visible. And if fails,
+                // the user should not be in the mediator's started users structure
+                mInjector.getUserManagerInternal().assignUserToDisplay(userId,
+                        userInfo.profileGroupId, foreground, displayId);
+            }
 
             // TODO(b/239982558): might need something similar for bg users on secondary display
             if (foreground && isUserSwitchUiEnabled()) {
@@ -1687,12 +1707,23 @@
                 // Make sure the old user is no longer considering the display to be on.
                 mInjector.reportGlobalUsageEvent(UsageEvents.Event.SCREEN_NON_INTERACTIVE);
                 boolean userSwitchUiEnabled;
+                // TODO(b/244333150): temporary state until the callback logic is moved to
+                // UserVisibilityManager
+                int previousCurrentUserId; boolean notifyPreviousCurrentUserId;
                 synchronized (mLock) {
+                    previousCurrentUserId = mCurrentUserId;
+                    notifyPreviousCurrentUserId = mVisibleUsers.get(previousCurrentUserId);
+                    if (notifyPreviousCurrentUserId) {
+                        deleteVisibleUserLocked(previousCurrentUserId);
+                    }
                     mCurrentUserId = userId;
                     mTargetUserId = UserHandle.USER_NULL; // reset, mCurrentUserId has caught up
                     userSwitchUiEnabled = mUserSwitchUiEnabled;
                 }
                 mInjector.updateUserConfiguration();
+                // TODO(b/244644281): updateProfileRelatedCaches() is called on both if and else
+                // parts, ideally it should be moved outside, but for now it's not as there are many
+                // calls to external components here afterwards
                 updateProfileRelatedCaches();
                 mInjector.getWindowManager().setCurrentUser(userId);
                 mInjector.reportCurWakefulnessUsageEvent();
@@ -1705,6 +1736,11 @@
                         mInjector.getWindowManager().lockNow(null);
                     }
                 }
+                if (notifyPreviousCurrentUserId) {
+                    mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG,
+                            previousCurrentUserId, 0));
+                }
+
             } else {
                 final Integer currentUserIdInt = mCurrentUserId;
                 updateProfileRelatedCaches();
@@ -1730,10 +1766,7 @@
                             && mInjector.getUserManagerInternal().isUserVisible(userId);
             if (visible) {
                 synchronized (mLock) {
-                    if (DEBUG_MU) {
-                        Slogf.d(TAG, "Adding %d to mVisibleUsers", userId);
-                    }
-                    mVisibleUsers.put(userId, true);
+                    addVisibleUserLocked(userId);
                 }
             }
 
@@ -1776,12 +1809,15 @@
                 mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId,
                         visible ? 1 : 0));
                 t.traceEnd();
-            } else if (visible) {
+            }
+
+            if (visible) {
                 // User was already running and became visible (for example, when switching to a
                 // user that was started in the background before), so it's necessary to explicitly
                 // notify the services (while when the user starts from BOOTING, USER_START_MSG
                 // takes care of that.
-                mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBLE_MSG, userId, NO_ARG2));
+                mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, userId,
+                        visible ? 1 : 0));
             }
 
             t.traceBegin("sendMessages");
@@ -2084,6 +2120,8 @@
     }
 
     private void timeoutUserSwitch(UserState uss, int oldUserId, int newUserId) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("timeoutUserSwitch-" + oldUserId + "-to-" + newUserId);
         synchronized (mLock) {
             Slogf.e(TAG, "User switch timeout: from " + oldUserId + " to " + newUserId);
             mTimeoutUserSwitchCallbacks = mCurWaitingUserSwitchCallbacks;
@@ -2093,6 +2131,7 @@
             mHandler.sendMessageDelayed(mHandler.obtainMessage(USER_SWITCH_CALLBACKS_TIMEOUT_MSG,
                     oldUserId, newUserId), USER_SWITCH_CALLBACKS_TIMEOUT_MS);
         }
+        t.traceEnd();
     }
 
     private void timeoutUserSwitchCallbacks(int oldUserId, int newUserId) {
@@ -2150,6 +2189,8 @@
                                             + " ms after dispatchUserSwitch.");
                                 }
 
+                                TimingsTraceAndSlog t2 = new TimingsTraceAndSlog(TAG);
+                                t2.traceBegin("onUserSwitchingReply-" + name);
                                 curWaitingUserSwitchCallbacks.remove(name);
                                 // Continue switching if all callbacks have been notified and
                                 // user switching session is still valid
@@ -2158,11 +2199,15 @@
                                         == mCurWaitingUserSwitchCallbacks)) {
                                     sendContinueUserSwitchLU(uss, oldUserId, newUserId);
                                 }
+                                t2.traceEnd();
                             }
                         }
                     };
+                    t.traceBegin("onUserSwitching-" + name);
                     mUserSwitchObservers.getBroadcastItem(i).onUserSwitching(newUserId, callback);
+                    t.traceEnd();
                 } catch (RemoteException e) {
+                    // Ignore
                 }
             }
         } else {
@@ -2176,10 +2221,13 @@
 
     @GuardedBy("mLock")
     private void sendContinueUserSwitchLU(UserState uss, int oldUserId, int newUserId) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("sendContinueUserSwitchLU-" + oldUserId + "-to-" + newUserId);
         mCurWaitingUserSwitchCallbacks = null;
         mHandler.removeMessages(USER_SWITCH_TIMEOUT_MSG);
         mHandler.sendMessage(mHandler.obtainMessage(CONTINUE_USER_SWITCH_MSG,
                 oldUserId, newUserId, uss));
+        t.traceEnd();
     }
 
     @VisibleForTesting
@@ -2561,7 +2609,8 @@
         if (!UserManager.isHeadlessSystemUserMode()) {
             // Don't need to call on HSUM because it will be called when the system user is
             // restarted on background
-            mInjector.onUserStarting(UserHandle.USER_SYSTEM, /* visible= */ true);
+            mInjector.onUserStarting(UserHandle.USER_SYSTEM);
+            mInjector.onUserVisibilityChanged(UserHandle.USER_SYSTEM, /* visible= */ true);
         }
     }
 
@@ -2573,12 +2622,12 @@
         int userId = UserHandle.USER_SYSTEM;
         synchronized (mLock) {
             if (visible) {
-                mVisibleUsers.put(userId, true);
+                addVisibleUserLocked(userId);
             } else {
-                mVisibleUsers.delete(userId);
+                deleteVisibleUserLocked(userId);
             }
         }
-        mInjector.notifySystemUserVisibilityChanged(visible);
+        mInjector.onUserVisibilityChanged(userId, visible);
         t.traceEnd();
     }
 
@@ -3078,7 +3127,7 @@
                 logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER,
                         USER_LIFECYCLE_EVENT_STATE_BEGIN);
 
-                mInjector.onUserStarting(/* userId= */ msg.arg1, /* visible= */ msg.arg2 == 1);
+                mInjector.onUserStarting(/* userId= */ msg.arg1);
                 scheduleOnUserCompletedEvent(msg.arg1,
                         UserCompletedEventType.EVENT_TYPE_USER_STARTING,
                         USER_COMPLETED_EVENT_DELAY_MS);
@@ -3159,8 +3208,9 @@
             case COMPLETE_USER_SWITCH_MSG:
                 completeUserSwitch(msg.arg1);
                 break;
-            case USER_VISIBLE_MSG:
-                mInjector.getSystemServiceManager().onUserVisible(/* userId= */ msg.arg1);
+            case USER_VISIBILITY_CHANGED_MSG:
+                mInjector.onUserVisibilityChanged(/* userId= */ msg.arg1,
+                        /* visible= */ msg.arg2 == 1);
                 break;
         }
         return false;
@@ -3692,12 +3742,12 @@
             return UserManager.isUsersOnSecondaryDisplaysEnabled();
         }
 
-        void onUserStarting(int userId, boolean visible) {
-            getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId,
-                    visible);
+        void onUserStarting(@UserIdInt int userId) {
+            getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId);
         }
-        void notifySystemUserVisibilityChanged(boolean visible) {
-            getSystemServiceManager().onSystemUserVisibilityChanged(visible);
+
+        void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+            getUserManagerInternal().onUserVisibilityChanged(userId, visible);
         }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index d35d193..78b697d 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1716,7 +1716,20 @@
         final Point userPreferredResolution =
                 mPersistentDataStore.getUserPreferredResolution(device);
         final float refreshRate = mPersistentDataStore.getUserPreferredRefreshRate(device);
-        if (userPreferredResolution == null && Float.isNaN(refreshRate)) {
+        // If value in persistentDataStore is null, preserving the mode from systemPreferredMode.
+        // This is required because in some devices, user-preferred mode was not stored in
+        // persistentDataStore, but was stored in a config which is returned through
+        // systemPreferredMode.
+        if ((userPreferredResolution == null && Float.isNaN(refreshRate))
+                || (userPreferredResolution.equals(0, 0) && refreshRate == 0.0f)) {
+            Display.Mode systemPreferredMode = device.getSystemPreferredDisplayModeLocked();
+            if (systemPreferredMode == null) {
+                return;
+            }
+            storeModeInPersistentDataStoreLocked(
+                    display.getDisplayIdLocked(), systemPreferredMode.getPhysicalWidth(),
+                    systemPreferredMode.getPhysicalHeight(), systemPreferredMode.getRefreshRate());
+            device.setUserPreferredDisplayModeLocked(systemPreferredMode);
             return;
         }
         Display.Mode.Builder modeBuilder = new Display.Mode.Builder();
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index dcec0aa..2669d21 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -140,7 +140,9 @@
 import com.android.server.location.provider.proxy.ProxyLocationProvider;
 import com.android.server.location.settings.LocationSettings;
 import com.android.server.location.settings.LocationUserSettings;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
+import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -308,6 +310,10 @@
         permissionManagerInternal.setLocationExtraPackagesProvider(
                 userId -> mContext.getResources().getStringArray(
                         com.android.internal.R.array.config_locationExtraPackageNames));
+
+        // TODO(b/241604546): properly handle this callback
+        LocalServices.getService(UserManagerInternal.class).addUserVisibilityListener(
+                (u, v) -> Slogf.i(TAG, "onUserVisibilityChanged(): %d -> %b", u, v));
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 56ec8e4..9155830 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -77,6 +77,23 @@
     }
 
     /**
+     * Listener for {@link UserManager#isUserVisible() user visibility} changes.
+     */
+    public interface UserVisibilityListener {
+
+        /**
+         * Called when the {@link UserManager#isUserVisible() user visibility} changed.
+         *
+         * <p><b>Note:</b> this method is called independently of
+         * {@link com.android.server.SystemService} callbacks; for example, the call with
+         * {@code visible} {@code true} might be called before the
+         * {@link com.android.server.SystemService#onUserStarting(com.android.server.SystemService.TargetUser)}
+         * call.
+         */
+        void onUserVisibilityChanged(@UserIdInt int userId, boolean visible);
+    }
+
+    /**
      * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to set
      * restrictions enforced by the user.
      *
@@ -331,13 +348,18 @@
      * <p>On most devices this call will be a no-op, but it will be used on devices that support
      * multiple users on multiple displays (like automotives with passenger displays).
      *
+     * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user
+     * is started)
+     *
      * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to
      * check it. In fact, one of the intended clients for this method is
      * {@code DisplayManagerService}, which will call it when a virtual display is created (another
      * client is {@code UserController}, which will call it when a user is started).
-     *
      */
-    public abstract void assignUserToDisplay(@UserIdInt int userId, int displayId);
+    // TODO(b/244644281): rename to assignUserToDisplayOnStart() and make sure it's called on boot
+    // as well
+    public abstract void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId,
+            boolean foreground, int displayId);
 
     /**
      * Unassigns a user from its current display.
@@ -346,7 +368,7 @@
      * multiple users on multiple displays (like automotives with passenger displays).
      *
      * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user
-     * is stopped) and {@code DisplayManagerService} (when a virtual display is destroyed).
+     * is stopped).
      */
     public abstract void unassignUserFromDisplay(@UserIdInt int userId);
 
@@ -390,4 +412,13 @@
      * would make such call).
      */
     public abstract @UserIdInt int getUserAssignedToDisplay(int displayId);
+
+    /** Adds a {@link UserVisibilityListener}. */
+    public abstract void addUserVisibilityListener(UserVisibilityListener listener);
+
+    /** Removes a {@link UserVisibilityListener}. */
+    public abstract void removeUserVisibilityListener(UserVisibilityListener listener);
+
+    /** TODO(b/244333150): temporary method until UserVisibilityMediator handles that logic */
+    public abstract void onUserVisibilityChanged(@UserIdInt int userId, boolean visible);
 }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 0eff9e9..064282d 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -96,6 +96,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
+import android.util.EventLog;
 import android.util.IndentingPrintWriter;
 import android.util.IntArray;
 import android.util.Slog;
@@ -124,9 +125,11 @@
 import com.android.server.LocalServices;
 import com.android.server.LockGuard;
 import com.android.server.SystemService;
+import com.android.server.am.EventLogTags;
 import com.android.server.am.UserState;
 import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
 import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
+import com.android.server.pm.UserManagerInternal.UserVisibilityListener;
 import com.android.server.storage.DeviceStorageMonitorInternal;
 import com.android.server.utils.Slogf;
 import com.android.server.utils.TimingsTraceAndSlog;
@@ -504,6 +507,10 @@
     @GuardedBy("mUserLifecycleListeners")
     private final ArrayList<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>();
 
+    // TODO(b/244333150): temporary array, should belong to UserVisibilityMediator
+    @GuardedBy("mUserVisibilityListeners")
+    private final ArrayList<UserVisibilityListener> mUserVisibilityListeners = new ArrayList<>();
+
     private final LockPatternUtils mLockPatternUtils;
 
     private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK =
@@ -626,7 +633,7 @@
     @GuardedBy("mUserStates")
     private final WatchedUserStates mUserStates = new WatchedUserStates();
 
-    private final UserVisibilityMediator mUserVisibilityMediator;
+    private final UserVisibilityMediator mUserVisibilityMediator = new UserVisibilityMediator();
 
     private static UserManagerService sInstance;
 
@@ -749,7 +756,6 @@
         mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING);
         mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null;
         emulateSystemUserModeIfNeeded();
-        mUserVisibilityMediator = new UserVisibilityMediator(this);
     }
 
     void systemReady() {
@@ -6147,7 +6153,7 @@
                     dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime);
                     return;
                 case "--visibility-mediator":
-                    mUserVisibilityMediator.dump(pw);
+                    mUserVisibilityMediator.dump(pw, args);
                     return;
             }
         }
@@ -6213,7 +6219,7 @@
         } // synchronized (mPackagesLock)
 
         pw.println();
-        mUserVisibilityMediator.dump(pw);
+        mUserVisibilityMediator.dump(pw, args);
         pw.println();
 
         // Dump some capabilities
@@ -6250,6 +6256,9 @@
         synchronized (mUserLifecycleListeners) {
             pw.println("  user lifecycle events: " + mUserLifecycleListeners.size());
         }
+        synchronized (mUserVisibilityListeners) {
+            pw.println("  user visibility events: " + mUserVisibilityListeners.size());
+        }
 
         // Dump UserTypes
         pw.println();
@@ -6789,13 +6798,16 @@
         }
 
         @Override
-        public void assignUserToDisplay(@UserIdInt int userId, int displayId) {
-            mUserVisibilityMediator.assignUserToDisplay(userId, displayId);
+        public void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId,
+                boolean foreground, int displayId) {
+            mUserVisibilityMediator.startUser(userId, profileGroupId, foreground, displayId);
+            mUserVisibilityMediator.assignUserToDisplay(userId, profileGroupId, displayId);
         }
 
         @Override
         public void unassignUserFromDisplay(@UserIdInt int userId) {
             mUserVisibilityMediator.unassignUserFromDisplay(userId);
+            mUserVisibilityMediator.stopUser(userId);
         }
 
         @Override
@@ -6817,8 +6829,39 @@
         public @UserIdInt int getUserAssignedToDisplay(int displayId) {
             return mUserVisibilityMediator.getUserAssignedToDisplay(displayId);
         }
+
+        @Override
+        public void addUserVisibilityListener(UserVisibilityListener listener) {
+            synchronized (mUserVisibilityListeners) {
+                mUserVisibilityListeners.add(listener);
+            }
+        }
+
+        @Override
+        public void removeUserVisibilityListener(UserVisibilityListener listener) {
+            synchronized (mUserVisibilityListeners) {
+                mUserVisibilityListeners.remove(listener);
+            }
+        }
+
+        @Override
+        public void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+            EventLog.writeEvent(EventLogTags.UM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0);
+            mHandler.post(() -> {
+                UserVisibilityListener[] listeners;
+                synchronized (mUserVisibilityListeners) {
+                    listeners = new UserVisibilityListener[mUserVisibilityListeners.size()];
+                    mUserVisibilityListeners.toArray(listeners);
+                }
+                for (UserVisibilityListener listener : listeners) {
+                    listener.onUserVisibilityChanged(userId, visible);
+                }
+            });
+        }
     } // class LocalService
 
+
+
     /**
      * Check if user has restrictions
      * @param restriction restrictions to check
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index f725c48..0015fad 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -15,10 +15,18 @@
  */
 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.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.DebugUtils;
+import android.util.Dumpable;
 import android.util.IndentingPrintWriter;
 import android.util.SparseIntArray;
 import android.view.Display;
@@ -29,6 +37,8 @@
 import com.android.server.utils.Slogf;
 
 import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
 
 /**
  * Class responsible for deciding whether a user is visible (or visible for a given display).
@@ -36,72 +46,146 @@
  * <p>This class is thread safe.
  */
 // TODO(b/244644281): improve javadoc (for example, explain all cases / modes)
-public final class UserVisibilityMediator {
+public final class UserVisibilityMediator implements Dumpable {
 
     private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE
 
     private static final String TAG = UserVisibilityMediator.class.getSimpleName();
 
-    private final Object mLock = new Object();
+    private static final String PREFIX_START_USER_RESULT = "START_USER_";
 
-    // TODO(b/244644281): should not depend on service, but keep its own internal state (like
-    // current user and profile groups), but it is initially as the code was just moved from UMS
-    // "as is". Similarly, it shouldn't need to pass the SparseIntArray on constructor (which was
-    // added to UMS for testing purposes)
-    private final UserManagerService mService;
+    // 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)
+    @VisibleForTesting
+    static final int INITIAL_CURRENT_USER_ID = USER_CURRENT;
+
+    public static final int START_USER_RESULT_SUCCESS_VISIBLE = 1;
+    public static final int START_USER_RESULT_SUCCESS_INVISIBLE = 2;
+    public static final int START_USER_RESULT_FAILURE = -1;
+
+    @IntDef(flag = false, prefix = {PREFIX_START_USER_RESULT}, value = {
+            START_USER_RESULT_SUCCESS_VISIBLE,
+            START_USER_RESULT_SUCCESS_INVISIBLE,
+            START_USER_RESULT_FAILURE
+    })
+    public @interface StartUserResult {}
+
+    private final Object mLock = new Object();
 
     private final boolean mUsersOnSecondaryDisplaysEnabled;
 
+    @UserIdInt
+    @GuardedBy("mLock")
+    private int mCurrentUserId = INITIAL_CURRENT_USER_ID;
+
     @Nullable
     @GuardedBy("mLock")
-    private final SparseIntArray mUsersOnSecondaryDisplays;
+    private final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray();
 
-    UserVisibilityMediator(UserManagerService service) {
-        this(service, UserManager.isUsersOnSecondaryDisplaysEnabled(),
-                /* usersOnSecondaryDisplays= */ null);
+    /**
+     * Mapping from each started user to its profile group.
+     */
+    @GuardedBy("mLock")
+    private final SparseIntArray mStartedProfileGroupIds = new SparseIntArray();
+
+    UserVisibilityMediator() {
+        this(UserManager.isUsersOnSecondaryDisplaysEnabled());
     }
 
     @VisibleForTesting
-    UserVisibilityMediator(UserManagerService service, boolean usersOnSecondaryDisplaysEnabled,
-            @Nullable SparseIntArray usersOnSecondaryDisplays) {
-        mService = service;
+    UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled) {
         mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
-        if (mUsersOnSecondaryDisplaysEnabled) {
-            mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null
-                    ? new SparseIntArray() // default behavior
-                    : usersOnSecondaryDisplays; // passed by unit test
-        } else {
-            mUsersOnSecondaryDisplays = null;
+    }
+
+    /**
+     * TODO(b/244644281): merge with assignUserToDisplay() or add javadoc.
+     */
+    public @StartUserResult int startUser(@UserIdInt int userId, @UserIdInt int profileGroupId,
+            boolean foreground, int displayId) {
+        int actualProfileGroupId = profileGroupId == NO_PROFILE_GROUP_ID
+                ? userId
+                : profileGroupId;
+        if (DBG) {
+            Slogf.d(TAG, "startUser(%d, %d, %b, %d): actualProfileGroupId=%d",
+                    userId, profileGroupId, foreground, displayId, actualProfileGroupId);
+        }
+        if (foreground && displayId != DEFAULT_DISPLAY) {
+            Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start foreground user on "
+                    + "secondary display", userId, actualProfileGroupId, foreground, displayId);
+            return START_USER_RESULT_FAILURE;
+        }
+
+        int visibility;
+        synchronized (mLock) {
+            if (isProfile(userId, actualProfileGroupId)) {
+                if (displayId != DEFAULT_DISPLAY) {
+                    Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user on "
+                            + "secondary display", userId, actualProfileGroupId, foreground,
+                            displayId);
+                    return START_USER_RESULT_FAILURE;
+                }
+                if (foreground) {
+                    Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user in "
+                            + "foreground");
+                    return START_USER_RESULT_FAILURE;
+                } else {
+                    boolean isParentRunning = mStartedProfileGroupIds
+                            .get(actualProfileGroupId) == actualProfileGroupId;
+                    if (DBG) {
+                        Slogf.d(TAG, "profile parent running: %b", isParentRunning);
+                    }
+                    visibility = isParentRunning
+                            ? START_USER_RESULT_SUCCESS_VISIBLE
+                            : START_USER_RESULT_SUCCESS_INVISIBLE;
+                }
+            } else if (foreground) {
+                mCurrentUserId = userId;
+                visibility = START_USER_RESULT_SUCCESS_VISIBLE;
+            } else {
+                visibility = START_USER_RESULT_SUCCESS_INVISIBLE;
+            }
+            if (DBG) {
+                Slogf.d(TAG, "adding user / profile mapping (%d -> %d) and returning %s",
+                        userId, actualProfileGroupId, startUserResultToString(visibility));
+            }
+            mStartedProfileGroupIds.put(userId, actualProfileGroupId);
+        }
+        return visibility;
+    }
+
+    /**
+     * TODO(b/244644281): merge with unassignUserFromDisplay() or add javadoc (and unit tests)
+     */
+    public void stopUser(@UserIdInt int userId) {
+        if (DBG) {
+            Slogf.d(TAG, "stopUser(%d)", userId);
+        }
+        synchronized (mLock) {
+            mStartedProfileGroupIds.delete(userId);
         }
     }
 
     /**
      * See {@link UserManagerInternal#assignUserToDisplay(int, int)}.
      */
-    public void assignUserToDisplay(int userId, int displayId) {
+    public void assignUserToDisplay(int userId, int profileGroupId, int displayId) {
         if (DBG) {
-            Slogf.d(TAG, "assignUserToDisplay(%d, %d)", userId, displayId);
+            Slogf.d(TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplaysEnabled=%b",
+                    userId, displayId, mUsersOnSecondaryDisplaysEnabled);
         }
 
-        // NOTE: Using Boolean instead of boolean as it will be re-used below
-        Boolean isProfile = null;
-        if (displayId == Display.DEFAULT_DISPLAY) {
-            if (mUsersOnSecondaryDisplaysEnabled) {
-                // Profiles are only supported in the default display, but it cannot return yet
-                // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY
-                // (this is done indirectly below when it checks that the profile parent is the
-                // current user, as the current user is always assigned to the DEFAULT_DISPLAY).
-                isProfile = isProfileUnchecked(userId);
+        if (displayId == DEFAULT_DISPLAY
+                && (!mUsersOnSecondaryDisplaysEnabled || !isProfile(userId, profileGroupId))) {
+            // Don't need to do anything because methods (such as isUserVisible()) already
+            // know that the current user (and their profiles) is assigned to the default display.
+            // But on MUMD devices, it profiles are only supported in the default display, so it
+            // cannot return yet as it needs to check if the parent is also assigned to the
+            // DEFAULT_DISPLAY (this is done indirectly below when it checks that the profile parent
+            // is the current user, as the current user is always assigned to the DEFAULT_DISPLAY).
+            if (DBG) {
+                Slogf.d(TAG, "ignoring on default display");
             }
-            if (isProfile == null || !isProfile) {
-                // Don't need to do anything because methods (such as isUserVisible()) already
-                // know that the current user (and their profiles) is assigned to the default
-                // display.
-                if (DBG) {
-                    Slogf.d(TAG, "ignoring on default display");
-                }
-                return;
-            }
+            return;
         }
 
         if (!mUsersOnSecondaryDisplaysEnabled) {
@@ -119,24 +203,21 @@
         Preconditions.checkArgument(userId != currentUserId,
                 "Cannot assign current user (%d) to other displays", currentUserId);
 
-        if (isProfile == null) {
-            isProfile = isProfileUnchecked(userId);
-        }
-        synchronized (mLock) {
-            if (isProfile) {
-                // Profile can only start in the same display as parent. And for simplicity,
-                // that display must be the DEFAULT_DISPLAY.
-                Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
-                        "Profile user can only be started in the default display");
-                int parentUserId = getProfileParentId(userId);
-                Preconditions.checkArgument(parentUserId == currentUserId,
-                        "Only profile of current user can be assigned to a display");
-                if (DBG) {
-                    Slogf.d(TAG, "Ignoring profile user %d on default display", userId);
-                }
-                return;
+        if (isProfile(userId, profileGroupId)) {
+            // Profile can only start in the same display as parent. And for simplicity,
+            // that display must be the DEFAULT_DISPLAY.
+            Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
+                    "Profile user can only be started in the default display");
+            int parentUserId = getStartedProfileGroupId(userId);
+            Preconditions.checkArgument(parentUserId == currentUserId,
+                    "Only profile of current user can be assigned to a display");
+            if (DBG) {
+                Slogf.d(TAG, "Ignoring profile user %d on default display", userId);
             }
+            return;
+        }
 
+        synchronized (mLock) {
             // Check if display is available
             for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
                 int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i);
@@ -289,7 +370,7 @@
                     continue;
                 }
                 int userId = mUsersOnSecondaryDisplays.keyAt(i);
-                if (!isProfileUnchecked(userId)) {
+                if (!isStartedProfile(userId)) {
                     return userId;
                 } else if (DBG) {
                     Slogf.d(TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's "
@@ -307,23 +388,42 @@
     }
 
     private void dump(IndentingPrintWriter ipw) {
-        ipw.println("UserVisibilityManager");
+        ipw.println("UserVisibilityMediator");
         ipw.increaseIndent();
 
-        ipw.print("Supports users on secondary displays: ");
-        ipw.println(mUsersOnSecondaryDisplaysEnabled);
+        synchronized (mLock) {
+            ipw.print("Current user id: ");
+            ipw.println(mCurrentUserId);
 
-        if (mUsersOnSecondaryDisplaysEnabled) {
-            ipw.print("Users on secondary displays: ");
-            synchronized (mLock) {
-                ipw.println(mUsersOnSecondaryDisplays);
+            ipw.print("Number of started user / profile group mappings: ");
+            ipw.println(mStartedProfileGroupIds.size());
+            if (mStartedProfileGroupIds.size() > 0) {
+                ipw.increaseIndent();
+                for (int i = 0; i < mStartedProfileGroupIds.size(); i++) {
+                    ipw.print("User #");
+                    ipw.print(mStartedProfileGroupIds.keyAt(i));
+                    ipw.print(" -> profile #");
+                    ipw.println(mStartedProfileGroupIds.valueAt(i));
+                }
+                ipw.decreaseIndent();
+            }
+
+            ipw.print("Supports users on secondary displays: ");
+            ipw.println(mUsersOnSecondaryDisplaysEnabled);
+
+            if (mUsersOnSecondaryDisplaysEnabled) {
+                ipw.print("Users on secondary displays: ");
+                synchronized (mLock) {
+                    ipw.println(mUsersOnSecondaryDisplays);
+                }
             }
         }
 
         ipw.decreaseIndent();
     }
 
-    void dump(PrintWriter pw) {
+    @Override
+    public void dump(PrintWriter pw, String[] args) {
         if (pw instanceof IndentingPrintWriter) {
             dump((IndentingPrintWriter) pw);
             return;
@@ -331,20 +431,70 @@
         dump(new IndentingPrintWriter(pw));
     }
 
-    // TODO(b/244644281): remove methods below once this class caches that state
-    private @UserIdInt int getCurrentUserId() {
-        return mService.getCurrentUserId();
+    @VisibleForTesting
+    Map<Integer, Integer> getUsersOnSecondaryDisplays() {
+        Map<Integer, Integer> map;
+        synchronized (mLock) {
+            int size = mUsersOnSecondaryDisplays.size();
+            map = new LinkedHashMap<>(size);
+            for (int i = 0; i < size; i++) {
+                map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i));
+            }
+        }
+        Slogf.v(TAG, "getUsersOnSecondaryDisplays(): returning %s", map);
+        return map;
     }
 
-    private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
-        return mService.isCurrentUserOrRunningProfileOfCurrentUser(userId);
+    /**
+     * Gets the user-friendly representation of the {@code result}.
+     */
+    public static String startUserResultToString(@StartUserResult int result) {
+        return DebugUtils.constantToString(UserVisibilityMediator.class, PREFIX_START_USER_RESULT,
+                result);
     }
 
-    private boolean isProfileUnchecked(@UserIdInt int userId) {
-        return mService.isProfileUnchecked(userId);
+    // TODO(b/244644281): methods below are needed because some APIs use the current users (full and
+    // profiles) state to decide whether a user is visible or not. If we decide to always store that
+    // info into intermediate maps, we should remove them.
+
+    @VisibleForTesting
+    @UserIdInt int getCurrentUserId() {
+        synchronized (mLock) {
+            return mCurrentUserId;
+        }
     }
 
-    private @UserIdInt int getProfileParentId(@UserIdInt int userId) {
-        return mService.getProfileParentId(userId);
+    @VisibleForTesting
+    boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            // Special case as NO_PROFILE_GROUP_ID == USER_NULL
+            if (userId == USER_NULL || mCurrentUserId == USER_NULL) {
+                return false;
+            }
+            if (mCurrentUserId == userId) {
+                return true;
+            }
+            return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID) == mCurrentUserId;
+        }
+    }
+
+    private static boolean isProfile(@UserIdInt int userId, @UserIdInt int profileGroupId) {
+        return profileGroupId != NO_PROFILE_GROUP_ID && profileGroupId != userId;
+    }
+
+    @VisibleForTesting
+    boolean isStartedProfile(@UserIdInt int userId) {
+        int profileGroupId;
+        synchronized (mLock) {
+            profileGroupId = mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+        }
+        return isProfile(userId, profileGroupId);
+    }
+
+    @VisibleForTesting
+    @UserIdInt int getStartedProfileGroupId(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
index 8582f54..2d76c50 100644
--- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java
+++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
@@ -127,7 +127,7 @@
      */
     public void notifyPowerPressed() {
         Log.i(TAG, "notifyPowerPressed");
-        if (mFingerprintManager == null) {
+        if (mFingerprintManager == null && mSideFpsEventHandlerReady.get()) {
             mFingerprintManager = mContext.getSystemService(FingerprintManager.class);
         }
         if (mFingerprintManager == null) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 7dc4f97..b8cd8d9 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1157,6 +1157,8 @@
                     Slog.w(TAG, "WallpaperService is not connected yet");
                     return;
                 }
+                TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+                t.traceBegin("WPMS.connectLocked-" + wallpaper.wallpaperComponent);
                 if (DEBUG) Slog.v(TAG, "Adding window token: " + mToken);
                 mWindowManagerInternal.addWindowToken(mToken, TYPE_WALLPAPER, mDisplayId,
                         null /* options */);
@@ -1173,6 +1175,7 @@
                                 false /* fromUser */, wallpaper, null /* reply */);
                     }
                 }
+                t.traceEnd();
             }
 
             void disconnectLocked() {
@@ -1322,6 +1325,8 @@
 
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
+            TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+            t.traceBegin("WPMS.onServiceConnected-" + name);
             synchronized (mLock) {
                 if (mWallpaper.connection == this) {
                     mService = IWallpaperService.Stub.asInterface(service);
@@ -1338,6 +1343,7 @@
                     mContext.getMainThreadHandler().removeCallbacks(mDisconnectRunnable);
                 }
             }
+            t.traceEnd();
         }
 
         @Override
@@ -1545,6 +1551,8 @@
         public void engineShown(IWallpaperEngine engine) {
             synchronized (mLock) {
                 if (mReply != null) {
+                    TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+                    t.traceBegin("WPMS.mReply.sendResult");
                     final long ident = Binder.clearCallingIdentity();
                     try {
                         mReply.sendResult(null);
@@ -1553,6 +1561,7 @@
                     } finally {
                         Binder.restoreCallingIdentity(ident);
                     }
+                    t.traceEnd();
                     mReply = null;
                 }
             }
@@ -3058,6 +3067,8 @@
             return true;
         }
 
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.bindWallpaperComponentLocked-" + componentName);
         try {
             if (componentName == null) {
                 componentName = mDefaultWallpaperComponent;
@@ -3190,6 +3201,8 @@
             }
             Slog.w(TAG, msg);
             return false;
+        } finally {
+            t.traceEnd();
         }
         return true;
     }
@@ -3234,7 +3247,10 @@
     }
 
     private void attachServiceLocked(WallpaperConnection conn, WallpaperData wallpaper) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.attachServiceLocked");
         conn.forEachDisplayConnector(connector-> connector.connectLocked(conn, wallpaper));
+        t.traceEnd();
     }
 
     private void notifyCallbacksLocked(WallpaperData wallpaper) {
@@ -3360,6 +3376,8 @@
     }
 
     void saveSettingsLocked(int userId) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.saveSettingsLocked-" + userId);
         JournaledFile journal = makeJournaledFile(userId);
         FileOutputStream fstream = null;
         try {
@@ -3388,6 +3406,7 @@
             IoUtils.closeQuietly(fstream);
             journal.rollback();
         }
+        t.traceEnd();
     }
 
 
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index ea82417..74a236b 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -501,12 +501,16 @@
 
             if (hasVisibleTaskbar(mainWindow)) {
                 cropBounds = new Rect(mActivityRecord.getBounds());
+
+                // Rounded corners should be displayed above the taskbar.
+                // It is important to call adjustBoundsForTaskbarUnchecked before offsetTo
+                // because taskbar bounds are in screen coordinates
+                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
+
                 // Activity bounds are in screen coordinates while (0,0) for activity's surface
                 // control is at the top left corner of an app window so offsetting bounds
                 // accordingly.
                 cropBounds.offsetTo(0, 0);
-                // Rounded corners should be displayed above the taskbar.
-                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
             }
 
             transaction
diff --git a/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java b/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java
new file mode 100644
index 0000000..33275bd
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import android.util.Dumpable;
+import android.util.Log;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@code JUnit} rule that logs (using tag {@value #TAG} the contents of
+ * {@link Dumpable dumpables} in case of failure.
+ */
+public final class DumpableDumperRule implements TestRule {
+
+    private static final String TAG = DumpableDumperRule.class.getSimpleName();
+
+    private static final String[] NO_ARGS = {};
+
+    private final List<Dumpable> mDumpables = new ArrayList<>();
+
+    /**
+     * Adds a {@link Dumpable} to be logged if the test case fails.
+     */
+    public void addDumpable(Dumpable dumpable) {
+        mDumpables.add(dumpable);
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    base.evaluate();
+                } catch (Throwable t) {
+                    dumpOnFailure(description.getMethodName());
+                    throw t;
+                }
+            }
+        };
+    }
+
+    private void dumpOnFailure(String testName) throws IOException {
+        if (mDumpables.isEmpty()) {
+            return;
+        }
+        Log.w(TAG, "Dumping " + mDumpables.size() + " dumpables on failure of " + testName);
+        mDumpables.forEach(d -> logDumpable(d));
+    }
+
+    private void logDumpable(Dumpable dumpable) {
+        try {
+            try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
+                dumpable.dump(pw, NO_ARGS);
+                String[] dump = sw.toString().split(System.lineSeparator());
+                Log.w(TAG, "Dumping " + dumpable.getDumpableName() + " (" + dump.length
+                        + " lines):");
+                for (String line : dump) {
+                    Log.w(TAG, line);
+                }
+
+            } catch (RuntimeException e) {
+                Log.e(TAG, "RuntimeException dumping " + dumpable.getDumpableName(), e);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "IOException dumping " + dumpable.getDumpableName(), e);
+        }
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java
index 9aa28ce..c0b5070 100644
--- a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java
@@ -23,6 +23,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
@@ -38,6 +39,9 @@
 
     private MockitoSession mSession;
 
+    @Rule
+    public final DumpableDumperRule mDumpableDumperRule = new DumpableDumperRule();
+
     @Before
     public void startSession() {
         if (DEBUG) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
index 21f541f..923c3e3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -44,14 +44,14 @@
 
     @Test
     public void testAssignUserToDisplay_systemUser() {
-        assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID));
+        assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(USER_SYSTEM, USER_SYSTEM, SECONDARY_DISPLAY_ID));
     }
 
     @Test
     public void testAssignUserToDisplay_invalidDisplay() {
         assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(USER_ID, INVALID_DISPLAY));
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, INVALID_DISPLAY));
     }
 
     @Test
@@ -59,7 +59,7 @@
         mockCurrentUser(USER_ID);
 
         assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID));
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID));
 
         assertNoUserAssignedToDisplay();
     }
@@ -67,11 +67,10 @@
     @Test
     public void testAssignUserToDisplay_startedProfileOfCurrentUser() {
         mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
         startDefaultProfile();
 
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
 
         Log.v(TAG, "Exception: " + e);
         assertNoUserAssignedToDisplay();
@@ -80,11 +79,10 @@
     @Test
     public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() {
         mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
         stopDefaultProfile();
 
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
 
         Log.v(TAG, "Exception: " + e);
         assertNoUserAssignedToDisplay();
@@ -92,17 +90,17 @@
 
     @Test
     public void testAssignUserToDisplay_displayAvailable() {
-        mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+        mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID);
 
         assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
     }
 
     @Test
     public void testAssignUserToDisplay_displayAlreadyAssigned() {
-        mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+        mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID);
 
-        IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> mMediator.assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID));
+        IllegalStateException e = assertThrows(IllegalStateException.class, () -> mMediator
+                .assignUserToDisplay(OTHER_USER_ID, OTHER_USER_ID, SECONDARY_DISPLAY_ID));
 
         Log.v(TAG, "Exception: " + e);
         assertWithMessage("exception (%s) message", e).that(e).hasMessageThat()
@@ -112,10 +110,10 @@
 
     @Test
     public void testAssignUserToDisplay_userAlreadyAssigned() {
-        mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+        mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID);
 
         IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> mMediator.assignUserToDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID));
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, OTHER_SECONDARY_DISPLAY_ID));
 
         Log.v(TAG, "Exception: " + e);
         assertWithMessage("exception (%s) message", e).that(e).hasMessageThat()
@@ -127,11 +125,9 @@
 
     @Test
     public void testAssignUserToDisplay_profileOnSameDisplayAsParent() {
-        addDefaultProfileAndParent();
-
-        mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+        mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
 
         Log.v(TAG, "Exception: " + e);
         assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
@@ -139,11 +135,9 @@
 
     @Test
     public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() {
-        addDefaultProfileAndParent();
-
-        mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID));
+        mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID));
 
         Log.v(TAG, "Exception: " + e);
         assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
@@ -151,11 +145,9 @@
 
     @Test
     public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() {
-        addDefaultProfileAndParent();
-
-        mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY));
+        mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, DEFAULT_DISPLAY));
 
         Log.v(TAG, "Exception: " + e);
         assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
@@ -201,7 +193,6 @@
 
     @Test
     public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() {
-        addDefaultProfileAndParent();
         startDefaultProfile();
         mockCurrentUser(PARENT_USER_ID);
         assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
@@ -212,7 +203,6 @@
 
     @Test
     public void testIsUserVisibleOnDisplay_stoppedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() {
-        addDefaultProfileAndParent();
         stopDefaultProfile();
         mockCurrentUser(PARENT_USER_ID);
         assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
@@ -223,7 +213,6 @@
 
     @Test
     public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserOnUnassignedSecondaryDisplay() {
-        addDefaultProfileAndParent();
         startDefaultProfile();
         mockCurrentUser(PARENT_USER_ID);
 
@@ -285,19 +274,6 @@
                 .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
     }
 
-    // TODO(b/244644281): scenario below shouldn't happen on "real life", as the profile cannot be
-    // started on secondary display if its parent isn't, so we might need to remove (or refactor
-    // this test) if/when the underlying logic changes
-    @Test
-    public void testGetUserAssignedToDisplay_profileOnSecondaryDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(USER_ID);
-        assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
-                .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
-    }
-
     // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
     // getUserAssignedToDisplay() for bg users relies only on the user / display assignments
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
index 7ae8117..7af5f5d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -39,27 +39,25 @@
         mockCurrentUser(USER_ID);
 
         assertThrows(UnsupportedOperationException.class,
-                () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID));
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID));
     }
 
     @Test
     public void testAssignUserToDisplay_otherDisplay_startProfileOfcurrentUser() {
         mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
         startDefaultProfile();
 
-        assertThrows(UnsupportedOperationException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+        assertThrows(UnsupportedOperationException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
     }
 
     @Test
     public void testAssignUserToDisplay_otherDisplay_stoppedProfileOfcurrentUser() {
         mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
         stopDefaultProfile();
 
-        assertThrows(UnsupportedOperationException.class,
-                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+        assertThrows(UnsupportedOperationException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
index 22e6e0d..7b20092 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -15,23 +15,27 @@
  */
 package com.android.server.pm;
 
+import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID;
 import static android.os.UserHandle.USER_NULL;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKED;
+import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
+import static com.android.server.pm.UserVisibilityMediator.START_USER_RESULT_FAILURE;
+import static com.android.server.pm.UserVisibilityMediator.START_USER_RESULT_SUCCESS_INVISIBLE;
+import static com.android.server.pm.UserVisibilityMediator.START_USER_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserVisibilityMediator.startUserResultToString;
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.annotation.UserIdInt;
-import android.util.SparseIntArray;
+import android.util.Log;
+
+import com.android.server.ExtendedMockitoTestCase;
 
 import org.junit.Before;
 import org.junit.Test;
 
-import java.util.LinkedHashMap;
-import java.util.Map;
-
 /**
  * Base class for {@link UserVisibilityMediator} tests.
  *
@@ -39,7 +43,33 @@
  * device mode (for example, whether the device supports concurrent multiple users on multiple
  * displays or not).
  */
-abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrInternalTestCase {
+abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase {
+
+    private static final String TAG = UserVisibilityMediatorTestCase.class.getSimpleName();
+
+    /**
+     * Id for a simple user (that doesn't have profiles).
+     */
+    protected static final int USER_ID = 600;
+
+    /**
+     * Id for another simple user.
+     */
+    protected static final int OTHER_USER_ID = 666;
+
+    /**
+     * Id for a user that has one profile (whose id is {@link #PROFILE_USER_ID}.
+     *
+     * <p>You can use {@link #addDefaultProfileAndParent()} to add both of this user to the service.
+     */
+    protected static final int PARENT_USER_ID = 642;
+
+    /**
+     * Id for a profile whose parent is {@link #PARENTUSER_ID}.
+     *
+     * <p>You can use {@link #addDefaultProfileAndParent()} to add both of this user to the service.
+     */
+    protected static final int PROFILE_USER_ID = 643;
 
     /**
      * Id of a secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}).
@@ -51,12 +81,10 @@
      */
     protected static final int OTHER_SECONDARY_DISPLAY_ID = 108;
 
-    private final boolean mUsersOnSecondaryDisplaysEnabled;
+    private static final boolean FG = true;
+    private static final boolean BG = false;
 
-    // TODO(b/244644281): manipulating mUsersOnSecondaryDisplays directly leaks implementation
-    // details into the unit test, but it's fine for now as the tests were copied "as is" - it
-    // would be better to use a geter() instead
-    protected final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray();
+    private final boolean mUsersOnSecondaryDisplaysEnabled;
 
     protected UserVisibilityMediator mMediator;
 
@@ -66,13 +94,93 @@
 
     @Before
     public final void setMediator() {
-        mMediator = new UserVisibilityMediator(mUms, mUsersOnSecondaryDisplaysEnabled,
-                mUsersOnSecondaryDisplays);
+        mMediator = new UserVisibilityMediator(mUsersOnSecondaryDisplaysEnabled);
+        mDumpableDumperRule.addDumpable(mMediator);
+    }
+
+    @Test
+    public final void testStartUser_currentUser() {
+        int result = mMediator.startUser(USER_ID, USER_ID, FG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE);
+
+        assertCurrentUser(USER_ID);
+        assertIsCurrentUserOrRunningProfileOfCurrentUser(USER_ID);
+        assertStartedProfileGroupIdOf(USER_ID, USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_currentUserSecondaryDisplay() {
+        int result = mMediator.startUser(USER_ID, USER_ID, FG, SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, START_USER_RESULT_FAILURE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(USER_ID);
+        assertStartedProfileGroupIdOf(USER_ID, NO_PROFILE_GROUP_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileBg_parentStarted() {
+        mockCurrentUser(PARENT_USER_ID);
+
+        int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE);
+
+        assertCurrentUser(PARENT_USER_ID);
+        assertIsCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+        assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID);
+        assertIsStartedProfile(PROFILE_USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileBg_parentNotStarted() {
+        int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, START_USER_RESULT_SUCCESS_INVISIBLE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+        assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID);
+        assertIsStartedProfile(PROFILE_USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileBg_secondaryDisplay() {
+        int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, START_USER_RESULT_FAILURE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileFg() {
+        int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, FG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, START_USER_RESULT_FAILURE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+        assertStartedProfileGroupIdOf(PROFILE_USER_ID, NO_PROFILE_GROUP_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileFgSecondaryDisplay() {
+        int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, FG, SECONDARY_DISPLAY_ID);
+
+        assertStartUserResult(result, START_USER_RESULT_FAILURE);
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+    }
+
+    @Test
+    public final void testGetStartedProfileGroupId_whenStartedWithNoProfileGroupId() {
+        int result = mMediator.startUser(USER_ID, NO_PROFILE_GROUP_ID, FG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE);
+
+        assertWithMessage("shit").that(mMediator.getStartedProfileGroupId(USER_ID))
+                .isEqualTo(USER_ID);
     }
 
     @Test
     public final void testAssignUserToDisplay_defaultDisplayIgnored() {
-        mMediator.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY);
+        mMediator.assignUserToDisplay(USER_ID, USER_ID, DEFAULT_DISPLAY);
 
         assertNoUserAssignedToDisplay();
     }
@@ -103,18 +211,14 @@
 
     @Test
     public final void testIsUserVisible_startedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         startDefaultProfile();
-        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
-
         assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID)
                 .that(mMediator.isUserVisible(PROFILE_USER_ID)).isTrue();
     }
 
     @Test
     public final void testIsUserVisible_stoppedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         stopDefaultProfile();
 
@@ -164,7 +268,6 @@
 
     @Test
     public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserInvalidDisplay() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         startDefaultProfile();
 
@@ -174,7 +277,6 @@
 
     @Test
     public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserInvalidDisplay() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         stopDefaultProfile();
 
@@ -184,18 +286,14 @@
 
     @Test
     public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserDefaultDisplay() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         startDefaultProfile();
-        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
-
         assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY)
                 .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue();
     }
 
     @Test
     public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         stopDefaultProfile();
 
@@ -205,18 +303,14 @@
 
     @Test
     public final void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplay() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         startDefaultProfile();
-        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
-
         assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
                 .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
     }
 
     @Test
     public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserSecondaryDisplay() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         stopDefaultProfile();
 
@@ -250,11 +344,8 @@
 
     @Test
     public final void testGetDisplayAssignedToUser_startedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         startDefaultProfile();
-        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
-
         assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID)
                 .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID))
                 .isEqualTo(DEFAULT_DISPLAY);
@@ -262,7 +353,6 @@
 
     @Test
     public final void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
         mockCurrentUser(PARENT_USER_ID);
         stopDefaultProfile();
 
@@ -296,28 +386,96 @@
                 .isEqualTo(USER_ID);
     }
 
-    // NOTE: should only called by tests that indirectly needs to check user assignments (like
-    // isUserVisible), not by tests for the user assignment methods per se.
+    // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining
+    // it's not meant to be used to test startUser() itself.
+    protected void mockCurrentUser(@UserIdInt int userId) {
+        Log.d(TAG, "mockCurrentUser(" + userId + ")");
+        int result = mMediator.startUser(userId, userId, FG, DEFAULT_DISPLAY);
+        if (result != START_USER_RESULT_SUCCESS_VISIBLE) {
+            throw new IllegalStateException("Failed to mock current user " + userId
+                    + ": mediator returned " + startUserResultToString(result));
+        }
+    }
+
+    // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining
+    // it's not meant to be used to test startUser() itself.
+    protected void startDefaultProfile() {
+        mockCurrentUser(PARENT_USER_ID);
+        Log.d(TAG, "starting default profile (" + PROFILE_USER_ID + ") in background after starting"
+                + " its parent (" + PARENT_USER_ID + ") on foreground");
+
+        int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY);
+        if (result != START_USER_RESULT_SUCCESS_VISIBLE) {
+            throw new IllegalStateException("Failed to start profile user " + PROFILE_USER_ID
+                    + ": mediator returned " + startUserResultToString(result));
+        }
+    }
+
+    // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining
+    // it's not meant to be used to test stopUser() itself.
+    protected void stopDefaultProfile() {
+        Log.d(TAG, "stopping default profile");
+        mMediator.stopUser(PROFILE_USER_ID);
+    }
+
+    // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining
+    // it's not meant to be used to test assignUserToDisplay() itself.
     protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) {
-        mUsersOnSecondaryDisplays.put(userId, displayId);
+        Log.d(TAG, "assignUserToDisplay(" + userId + ", " + displayId + ")");
+        int result = mMediator.startUser(userId, userId, BG, displayId);
+        if (result != START_USER_RESULT_SUCCESS_INVISIBLE) {
+            throw new IllegalStateException("Failed to startuser " + userId
+                    + " on background: mediator returned " + startUserResultToString(result));
+        }
+        mMediator.assignUserToDisplay(userId, userId, displayId);
+
     }
 
     protected final void assertNoUserAssignedToDisplay() {
-        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
+        assertWithMessage("uses on secondary displays")
+                .that(mMediator.getUsersOnSecondaryDisplays())
                 .isEmpty();
     }
 
     protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) {
-        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
+        assertWithMessage("uses on secondary displays")
+                .that(mMediator.getUsersOnSecondaryDisplays())
                 .containsExactly(userId, displayId);
     }
 
-    private Map<Integer, Integer> usersOnSecondaryDisplaysAsMap() {
-        int size = mUsersOnSecondaryDisplays.size();
-        Map<Integer, Integer> map = new LinkedHashMap<>(size);
-        for (int i = 0; i < size; i++) {
-            map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i));
-        }
-        return map;
+    private void assertCurrentUser(@UserIdInt int userId) {
+        assertWithMessage("mediator.getCurrentUserId()").that(mMediator.getCurrentUserId())
+                .isEqualTo(userId);
+    }
+
+    private void assertIsStartedProfile(@UserIdInt int userId) {
+        assertWithMessage("mediator.isStartedProfile(%s)", userId)
+                .that(mMediator.isStartedProfile(userId))
+                .isTrue();
+    }
+
+    private void assertStartedProfileGroupIdOf(@UserIdInt int profileId, @UserIdInt int parentId) {
+        assertWithMessage("mediator.getStartedProfileGroupId(%s)", profileId)
+                .that(mMediator.getStartedProfileGroupId(profileId))
+                .isEqualTo(parentId);
+    }
+
+    private void assertIsCurrentUserOrRunningProfileOfCurrentUser(int userId) {
+        assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId)
+                .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId))
+                .isTrue();
+    }
+
+    private void assertIsNotCurrentUserOrRunningProfileOfCurrentUser(int userId) {
+        assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId)
+                .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId))
+                .isFalse();
+    }
+
+    private void assertStartUserResult(int actualResult, int expectedResult) {
+        assertWithMessage("startUser() result (where %s=%s and %s=%s)",
+                actualResult, startUserResultToString(actualResult),
+                expectedResult, startUserResultToString(expectedResult))
+                        .that(actualResult).isEqualTo(expectedResult);
     }
 }
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 61bb57e..9386a23 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -204,10 +204,10 @@
         ":FrameworksServicesTests_install_uses_sdk_q0",
         ":FrameworksServicesTests_install_uses_sdk_q0_r0",
         ":FrameworksServicesTests_install_uses_sdk_r0",
-        ":FrameworksServicesTests_install_uses_sdk_r5",
+        ":FrameworksServicesTests_install_uses_sdk_r1000",
         ":FrameworksServicesTests_install_uses_sdk_r_none",
         ":FrameworksServicesTests_install_uses_sdk_r0_s0",
-        ":FrameworksServicesTests_install_uses_sdk_r0_s5",
+        ":FrameworksServicesTests_install_uses_sdk_r0_s1000",
         ":FrameworksServicesTests_keyset_permdef_sa_unone",
         ":FrameworksServicesTests_keyset_permuse_sa_ua_ub",
         ":FrameworksServicesTests_keyset_permuse_sb_ua_ub",
diff --git a/services/tests/servicestests/apks/install_uses_sdk/Android.bp b/services/tests/servicestests/apks/install_uses_sdk/Android.bp
index a51293d..2894395 100644
--- a/services/tests/servicestests/apks/install_uses_sdk/Android.bp
+++ b/services/tests/servicestests/apks/install_uses_sdk/Android.bp
@@ -32,9 +32,9 @@
 }
 
 android_test_helper_app {
-    name: "FrameworksServicesTests_install_uses_sdk_r5",
+    name: "FrameworksServicesTests_install_uses_sdk_r1000",
     defaults: ["FrameworksServicesTests_apks_defaults"],
-    manifest: "AndroidManifest-r5.xml",
+    manifest: "AndroidManifest-r1000.xml",
 }
 
 android_test_helper_app {
@@ -44,9 +44,9 @@
 }
 
 android_test_helper_app {
-    name: "FrameworksServicesTests_install_uses_sdk_r0_s5",
+    name: "FrameworksServicesTests_install_uses_sdk_r0_s1000",
     defaults: ["FrameworksServicesTests_apks_defaults"],
-    manifest: "AndroidManifest-r0-s5.xml",
+    manifest: "AndroidManifest-r0-s1000.xml",
 }
 
 android_test_helper_app {
diff --git a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s5.xml b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
similarity index 97%
rename from services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s5.xml
rename to services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
index bafe4c4..25743b8 100644
--- a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s5.xml
+++ b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
@@ -19,7 +19,7 @@
     <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
         <!-- This fails because 31 is not version 5 -->
         <extension-sdk android:sdkVersion="30" android:minExtensionVersion="0" />
-        <extension-sdk android:sdkVersion="31" android:minExtensionVersion="5" />
+        <extension-sdk android:sdkVersion="31" android:minExtensionVersion="1000" />
     </uses-sdk>
 
     <application>
diff --git a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r5.xml b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r1000.xml
similarity index 97%
rename from services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r5.xml
rename to services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r1000.xml
index 7723d05..9bf9254 100644
--- a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r5.xml
+++ b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r1000.xml
@@ -18,7 +18,7 @@
 
     <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
         <!-- This will fail to install, because minExtensionVersion is not met -->
-        <extension-sdk android:sdkVersion="30" android:minExtensionVersion="5" />
+        <extension-sdk android:sdkVersion="30" android:minExtensionVersion="1000" />
     </uses-sdk>
 
     <application>
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 935d1d8..80cee50 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -38,6 +38,7 @@
 import static com.android.server.am.UserController.USER_CURRENT_MSG;
 import static com.android.server.am.UserController.USER_START_MSG;
 import static com.android.server.am.UserController.USER_SWITCH_TIMEOUT_MSG;
+import static com.android.server.am.UserController.USER_VISIBILITY_CHANGED_MSG;
 
 import static com.google.android.collect.Lists.newArrayList;
 import static com.google.android.collect.Sets.newHashSet;
@@ -158,6 +159,7 @@
             REPORT_USER_SWITCH_MSG,
             USER_SWITCH_TIMEOUT_MSG,
             USER_START_MSG,
+            USER_VISIBILITY_CHANGED_MSG,
             USER_CURRENT_MSG);
 
     private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
@@ -283,7 +285,7 @@
         assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes())
                 .containsExactly(USER_START_MSG);
 
-        verifyUserAssignedToDisplay(TEST_PRE_CREATED_USER_ID, Display.DEFAULT_DISPLAY);
+        verifyUserNeverAssignedToDisplay();
     }
 
     private void startUserAssertions(
@@ -948,11 +950,13 @@
     }
 
     private void verifyUserAssignedToDisplay(@UserIdInt int userId, int displayId) {
-        verify(mInjector.getUserManagerInternal()).assignUserToDisplay(userId, displayId);
+        verify(mInjector.getUserManagerInternal()).assignUserToDisplay(eq(userId), anyInt(),
+                anyBoolean(), eq(displayId));
     }
 
     private void verifyUserNeverAssignedToDisplay() {
-        verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplay(anyInt(), anyInt());
+        verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplay(anyInt(), anyInt(),
+                anyBoolean(), anyInt());
     }
 
     private void verifyUserUnassignedFromDisplay(@UserIdInt int userId) {
@@ -964,7 +968,7 @@
     }
 
     private void verifySystemUserVisibilityChangedNotified(boolean visible) {
-        verify(mInjector).notifySystemUserVisibilityChanged(visible);
+        verify(mInjector).onUserVisibilityChanged(UserHandle.USER_SYSTEM, visible);
     }
 
     // Should be public to allow mocking
@@ -1104,13 +1108,13 @@
         }
 
         @Override
-        void onUserStarting(@UserIdInt int userId, boolean visible) {
-            Log.i(TAG, "onUserStarting(" + userId + ", " + visible + ")");
+        void onUserStarting(@UserIdInt int userId) {
+            Log.i(TAG, "onUserStarting(" + userId + ")");
         }
 
         @Override
-        void notifySystemUserVisibilityChanged(boolean visible) {
-            Log.i(TAG, "notifySystemUserVisibilityChanged(" + visible + ")");
+        void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+            Log.i(TAG, "onUserVisibilityChanged(" + userId + ", " + visible + ")");
         }
     }
 
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/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java b/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
index 699601b..8e6c014 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
@@ -575,10 +575,11 @@
         assertEquals(0, minExtVers.get(31, -1));
 
         Map<Pair<String, Integer>, Integer> appToError = new HashMap<>();
-        appToError.put(Pair.create("install_uses_sdk.apk_r5", R.raw.install_uses_sdk_r5),
+        appToError.put(Pair.create("install_uses_sdk.apk_r1000", R.raw.install_uses_sdk_r1000),
                        PackageManager.INSTALL_FAILED_OLDER_SDK);
-        appToError.put(Pair.create("install_uses_sdk.apk_r0_s5", R.raw.install_uses_sdk_r0_s5),
-                       PackageManager.INSTALL_FAILED_OLDER_SDK);
+        appToError.put(
+                Pair.create("install_uses_sdk.apk_r0_s1000", R.raw.install_uses_sdk_r0_s1000),
+                PackageManager.INSTALL_FAILED_OLDER_SDK);
 
         appToError.put(Pair.create("install_uses_sdk.apk_q0", R.raw.install_uses_sdk_q0),
                        PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED);
@@ -595,7 +596,7 @@
             int result = entry.getValue();
             try {
                 parsePackage(filename, resId, x -> x);
-                expect.withMessage("Expected parsing error %d from %s", result, filename).fail();
+                expect.withMessage("Expected parsing error %s from %s", result, filename).fail();
             } catch (PackageManagerException expected) {
                 expect.that(expected.error).isEqualTo(result);
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index c906abc..e5842b4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -25,6 +25,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS;
+import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES;
 import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT;
@@ -71,6 +72,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.times;
 
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -87,6 +89,8 @@
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
 import android.view.InsetsFrameProvider;
+import android.view.InsetsSource;
+import android.view.InsetsVisibilities;
 import android.view.WindowManager;
 
 import androidx.test.filters.MediumTest;
@@ -105,6 +109,9 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.List;
 
 /**
  * Tests for Size Compatibility mode.
@@ -2368,6 +2375,48 @@
     }
 
     @Test
+    public void testLetterboxDetailsForTaskBar_letterboxNotOverlappingTaskBar() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        final int screenHeight = 2200;
+        final int screenWidth = 1400;
+        final int taskbarHeight = 200;
+        setUpDisplaySizeWithApp(screenWidth, screenHeight);
+
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+        // Move first activity to split screen which takes half of the screen.
+        organizer.mPrimary.setBounds(0, screenHeight / 2, screenWidth, screenHeight);
+        organizer.putTaskToPrimary(mTask, true);
+
+        final InsetsSource navSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
+
+        mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
+
+        final WindowState w1 = addWindowToActivity(mActivity);
+        w1.mAboveInsetsState.addSource(navSource);
+
+        // Prepare unresizable activity with max aspect ratio
+        prepareUnresizable(mActivity, /* maxAspect */ 1.1f, SCREEN_ORIENTATION_UNSPECIFIED);
+
+        // Refresh the letterboxes
+        mActivity.mRootWindowContainer.performSurfacePlacement();
+
+        final ArgumentCaptor<Rect> cropCapturer = ArgumentCaptor.forClass(Rect.class);
+        verify(mTransaction, times(2)).setWindowCrop(
+                eq(w1.getSurfaceControl()),
+                cropCapturer.capture()
+        );
+        final List<Rect> capturedCrops = cropCapturer.getAllValues();
+
+        final int expectedHeight = screenHeight / 2 - taskbarHeight;
+        assertEquals(2, capturedCrops.size());
+        assertEquals(expectedHeight, capturedCrops.get(0).bottom);
+        assertEquals(expectedHeight, capturedCrops.get(1).bottom);
+    }
+
+    @Test
     public void testSplitScreenLetterboxDetailsForStatusBar_twoLetterboxedApps() {
         mAtm.mDevEnableNonResizableMultiWindow = true;
         setUpDisplaySizeWithApp(2800, 1000);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 7f5beb1..4fd2b78 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -2392,6 +2392,8 @@
         @Override
         public void setAppStandbyBucket(String packageName, int bucket, int userId) {
 
+            super.setAppStandbyBucket_enforcePermission();
+
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final long token = Binder.clearCallingIdentity();
@@ -2442,6 +2444,8 @@
         @Override
         public void setAppStandbyBuckets(ParceledListSlice appBuckets, int userId) {
 
+            super.setAppStandbyBuckets_enforcePermission();
+
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final long token = Binder.clearCallingIdentity();
@@ -2493,6 +2497,8 @@
         public void setEstimatedLaunchTime(String packageName, long estimatedLaunchTime,
                 int userId) {
 
+            super.setEstimatedLaunchTime_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 UsageStatsService.this
@@ -2506,6 +2512,8 @@
         @Override
         public void setEstimatedLaunchTimes(ParceledListSlice estimatedLaunchTimes, int userId) {
 
+            super.setEstimatedLaunchTimes_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 UsageStatsService.this
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 453c656..936fad5 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8615,7 +8615,12 @@
      *
      * Used to trade privacy/security against potentially reduced carrier coverage for some
      * carriers.
+     *
+     * @deprecated Future versions of Android will disallow carriers from hiding this toggle
+     * because disabling 2g is a security feature that users should always have access to at
+     * their discretion.
      */
+    @Deprecated
     public static final String KEY_HIDE_ENABLE_2G = "hide_enable_2g_bool";
 
     /**
@@ -8780,7 +8785,7 @@
      * The default value is 30 minutes.
      *
      * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR
-     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED
      */
     public static final String
             KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG =
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index bfa60ba..6be2f77 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -138,13 +138,6 @@
      */
     public static final int FREQUENCY_RANGE_MMWAVE = 4;
 
-    private static final List<Integer> FREQUENCY_RANGE_ORDER = Arrays.asList(
-            FREQUENCY_RANGE_UNKNOWN,
-            FREQUENCY_RANGE_LOW,
-            FREQUENCY_RANGE_MID,
-            FREQUENCY_RANGE_HIGH,
-            FREQUENCY_RANGE_MMWAVE);
-
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "DUPLEX_MODE_",
@@ -2108,15 +2101,6 @@
     }
 
     /**
-     * @hide
-     */
-    public static final int getBetterNRFrequencyRange(int range1, int range2) {
-        return FREQUENCY_RANGE_ORDER.indexOf(range1) > FREQUENCY_RANGE_ORDER.indexOf(range2)
-                ? range1
-                : range2;
-    }
-
-    /**
      * Returns a copy of self with location-identifying information removed.
      * Always clears the NetworkRegistrationInfo's CellIdentity fields, but if removeCoarseLocation
      * is true, clears other info as well.
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 51a7840..e099e69 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17324,14 +17324,14 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12;
 
     /**
-     * Purchase premium capability failed because the network is congested.
+     * Purchase premium capability failed because the entitlement check failed.
      * Subsequent attempts will be throttled for the amount of time specified by
      * {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
      * and return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED}.
      * Throttling will be reevaluated when the network is no longer congested.
      */
-    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13;
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13;
 
     /**
      * Purchase premium capability failed because the request was not made on the default data
@@ -17368,7 +17368,7 @@
             PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP})
     public @interface PurchasePremiumCapabilityResult {}
@@ -17407,8 +17407,8 @@
                 return "REQUEST_FAILED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE:
                 return "NETWORK_NOT_AVAILABLE";
-            case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED:
-                return "NETWORK_CONGESTED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED:
+                return "ENTITLEMENT_CHECK_FAILED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB:
                 return "NOT_DEFAULT_DATA_SUB";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP:
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);
     }
 }