Merge "Turn off the verbose logging in the AppBatteryTracker" into tm-dev
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index f49cdbf..4710322 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -1777,7 +1777,10 @@
          * {@link Build.VERSION_CODES#S}, but starting from Android version
          * {@link Build.VERSION_CODES#TIRAMISU}, expedited jobs for the foreground app are
          * guaranteed to be started before {@link JobScheduler#schedule(JobInfo)} returns (assuming
-         * all requested constraints are satisfied), similar to foreground services.
+         * all requested constraints are satisfied), similar to foreground services. However, this
+         * start guarantee means there is a higher chance of overlapping executions, as noted in
+         * {@link JobService}, so be sure to handle that properly if you intend to reschedule the
+         * job while it's actively running.
          *
          * @see JobInfo#isExpedited()
          */
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 1f4ef04..388bbf1 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -107,7 +107,9 @@
     /**
      * Schedule a job to be executed.  Will replace any currently scheduled job with the same
      * ID with the new information in the {@link JobInfo}.  If a job with the given ID is currently
-     * running, it will be stopped.
+     * running, it will be stopped. Note that in some cases, the newly scheduled job may be started
+     * before the previously running job has been fully stopped. See {@link JobService} for
+     * additional details.
      *
      * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's
      * rescheduling the same job and the job didn't execute, especially on platform versions before
@@ -131,7 +133,7 @@
      * job.  If a job with the same ID is already scheduled, it will be replaced with the
      * new {@link JobInfo}, but any previously enqueued work will remain and be dispatched the
      * next time it runs.  If a job with the same ID is already running, the new work will be
-     * enqueued for it.
+     * enqueued for it without stopping the job.
      *
      * <p>The work you enqueue is later retrieved through
      * {@link JobParameters#dequeueWork() JobParameters.dequeueWork}.  Be sure to see there
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index e5b0742..7ed4b62 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -31,6 +31,17 @@
  * in blocking any future callbacks from the JobManager - specifically
  * {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the
  * scheduling requirements are no longer being met.</p>
+ *
+ * As a subclass of {@link Service}, there will only be one active instance of any JobService
+ * subclasses, regardless of job ID. This means that if you schedule multiple jobs with different
+ * job IDs but using the same JobService class, that JobService may receive multiple calls to
+ * {@link #onStartJob(JobParameters)} and {@link #onStopJob(JobParameters)}, with each call being
+ * for the separate jobs.
+ *
+ * <p class="note">Note that if you cancel and reschedule an already executing job,
+ * there may be a small period of time where {@link #onStartJob(JobParameters)} has been called for
+ * the newly scheduled job instance before {@link #onStopJob(JobParameters)} has been called or
+ * fully processed for the old job.</p>
  */
 public abstract class JobService extends Service {
     private static final String TAG = "JobService";
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index c86353c..afe36b5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -542,6 +542,22 @@
         return mRunningJobs.contains(job);
     }
 
+    /**
+     * Returns true if a job that is "similar" to the provided job is currently running.
+     * "Similar" in this context means any job that the {@link JobStore} would consider equivalent
+     * and replace one with the other.
+     */
+    @GuardedBy("mLock")
+    private boolean isSimilarJobRunningLocked(JobStatus job) {
+        for (int i = mRunningJobs.size() - 1; i >= 0; --i) {
+            JobStatus js = mRunningJobs.valueAt(i);
+            if (job.getUid() == js.getUid() && job.getJobId() == js.getJobId()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** Return {@code true} if the state was updated. */
     @GuardedBy("mLock")
     private boolean refreshSystemStateLocked() {
@@ -699,6 +715,21 @@
                 continue;
             }
 
+            final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob()
+                    && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP;
+            // Avoid overlapping job execution as much as possible.
+            if (!isTopEj && isSimilarJobRunningLocked(nextPending)) {
+                if (DEBUG) {
+                    Slog.w(TAG, "Delaying execution of job because of similarly running one: "
+                            + nextPending);
+                }
+                // It would be nice to let the JobService running the other similar job know about
+                // this new job so that it doesn't unbind from the JobService and we can call
+                // onStartJob as soon as the older job finishes.
+                // TODO: optimize the job reschedule flow to reduce service binding churn
+                continue;
+            }
+
             // Find an available slot for nextPending. The context should be one of the following:
             // 1. Unused
             // 2. Its job should have used up its minimum execution guarantee so it
@@ -707,8 +738,6 @@
             ContextAssignment selectedContext = null;
             final int allWorkTypes = getJobWorkTypes(nextPending);
             final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending);
-            final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob()
-                    && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP;
             final boolean isInOverage = projectedRunningCount > STANDARD_CONCURRENCY_LIMIT;
             boolean startingJob = false;
             if (idle.size() > 0) {
@@ -1177,6 +1206,15 @@
                     continue;
                 }
 
+                // Avoid overlapping job execution as much as possible.
+                if (isSimilarJobRunningLocked(nextPending)) {
+                    if (DEBUG) {
+                        Slog.w(TAG, "Avoiding execution of job because of similarly running one: "
+                                + nextPending);
+                    }
+                    continue;
+                }
+
                 if (worker.getPreferredUid() != nextPending.getUid()) {
                     if (backupJob == null && !isPkgConcurrencyLimitedLocked(nextPending)) {
                         int allWorkTypes = getJobWorkTypes(nextPending);
@@ -1260,6 +1298,15 @@
                     continue;
                 }
 
+                // Avoid overlapping job execution as much as possible.
+                if (isSimilarJobRunningLocked(nextPending)) {
+                    if (DEBUG) {
+                        Slog.w(TAG, "Avoiding execution of job because of similarly running one: "
+                                + nextPending);
+                    }
+                    continue;
+                }
+
                 if (isPkgConcurrencyLimitedLocked(nextPending)) {
                     continue;
                 }
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 358c327..cd70e88 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1208,12 +1208,22 @@
             // This may throw a SecurityException.
             jobStatus.prepareLocked();
 
+            final boolean canExecuteImmediately;
             if (toCancel != null) {
                 // Implicitly replaces the existing job record with the new instance
-                cancelJobImplLocked(toCancel, jobStatus, JobParameters.STOP_REASON_CANCELLED_BY_APP,
-                        JobParameters.INTERNAL_STOP_REASON_CANCELED, "job rescheduled by app");
+                final boolean wasJobExecuting = cancelJobImplLocked(toCancel, jobStatus,
+                        JobParameters.STOP_REASON_CANCELLED_BY_APP,
+                        JobParameters.INTERNAL_STOP_REASON_CANCELED,
+                        "job rescheduled by app");
+                // Avoid overlapping job executions. Don't push for immediate execution if an old
+                // job with the same ID was running, but let TOP EJs start immediately.
+                canExecuteImmediately = !wasJobExecuting
+                        || (jobStatus.isRequestedExpeditedJob()
+                        && mUidBiasOverride.get(jobStatus.getSourceUid(), JobInfo.BIAS_DEFAULT)
+                        == JobInfo.BIAS_TOP_APP);
             } else {
                 startTrackingJobLocked(jobStatus, null);
+                canExecuteImmediately = true;
             }
 
             if (work != null) {
@@ -1256,7 +1266,12 @@
                 // list and try to run it.
                 mJobPackageTracker.notePending(jobStatus);
                 mPendingJobQueue.add(jobStatus);
-                maybeRunPendingJobsLocked();
+                if (canExecuteImmediately) {
+                    // Don't ask the JobConcurrencyManager to try to run the job immediately. The
+                    // JobServiceContext will ask the JobConcurrencyManager for another job once
+                    // it finishes cleaning up the old job.
+                    maybeRunPendingJobsLocked();
+                }
             } else {
                 evaluateControllerStatesLocked(jobStatus);
             }
@@ -1377,8 +1392,10 @@
      * is null, the cancelled job is removed outright from the system.  If
      * {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of
      * currently scheduled jobs.
+     *
+     * @return true if the cancelled job was running
      */
-    private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob,
+    private boolean cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob,
             @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
         if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
         cancelled.unprepareLocked();
@@ -1389,7 +1406,7 @@
         }
         mChangedJobList.remove(cancelled);
         // Cancel if running.
-        mConcurrencyManager.stopJobOnServiceContextLocked(
+        boolean wasRunning = mConcurrencyManager.stopJobOnServiceContextLocked(
                 cancelled, reason, internalReasonCode, debugReason);
         // If this is a replacement, bring in the new version of the job
         if (incomingJob != null) {
@@ -1397,6 +1414,7 @@
             startTrackingJobLocked(incomingJob, cancelled);
         }
         reportActiveLocked();
+        return wasRunning;
     }
 
     void updateUidState(int uid, int procState) {
@@ -1755,7 +1773,7 @@
             // same job ID), we remove it from the JobStore and tell the JobServiceContext to stop
             // running the job. Once the job stops running, we then call this method again.
             // TODO: rework code so we don't intentionally call this method twice for the same job
-            Slog.w(TAG, "Job didn't exist in JobStore");
+            Slog.w(TAG, "Job didn't exist in JobStore: " + jobStatus.toShortString());
         }
         if (mReadyToRock) {
             for (int i = 0; i < mControllers.size(); i++) {
@@ -3813,6 +3831,7 @@
                     // Double indent for readability
                     pw.increaseIndent();
                     pw.increaseIndent();
+                    pw.println(job.toShortString());
                     job.dump(pw, true, nowElapsed);
                     pw.decreaseIndent();
                     pw.decreaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index 80f3fea..c90291e 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -979,6 +979,7 @@
             dumpBucketExpiryTimes(idpw, appUsageHistory, totalElapsedTime);
             idpw.print(" lastJob=");
             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
+            idpw.print(" lastInformedBucket=" + appUsageHistory.lastInformedBucket);
             if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) {
                 idpw.print(" lastRestrictAttempt=");
                 TimeUtils.formatDuration(
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index c3d6b73..1891e06 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1155,6 +1155,12 @@
 
         final int appId = getAppId(packageName);
         if (appId < 0) return;
+        final int minBucket = getAppMinBucket(packageName, appId, userId);
+        if (idle && minBucket < AppIdleHistory.IDLE_BUCKET_CUTOFF) {
+            Slog.e(TAG, "Tried to force an app to be idle when its min bucket is "
+                    + standbyBucketToString(minBucket));
+            return;
+        }
         final long elapsedRealtime = mInjector.elapsedRealtime();
 
         final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
@@ -1166,12 +1172,10 @@
         final boolean stillIdle = isAppIdleFiltered(packageName, appId,
                 userId, elapsedRealtime);
         // Inform listeners if necessary
+        maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket,
+                REASON_MAIN_FORCED_BY_USER, false);
         if (previouslyIdle != stillIdle) {
-            maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket,
-                    REASON_MAIN_FORCED_BY_USER, false);
-            if (!stillIdle) {
-                notifyBatteryStats(packageName, userId, idle);
-            }
+            notifyBatteryStats(packageName, userId, stillIdle);
         }
     }
 
@@ -1934,6 +1938,8 @@
             }
             mAppIdleHistory.setAppStandbyBucket(
                     packageName, userId, elapsedRealtime, newBucket, newReason);
+            maybeInformListeners(packageName, userId, elapsedRealtime, newBucket,
+                    newReason, false);
         }
     }
 
@@ -2490,6 +2496,8 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_INFORM_LISTENERS:
+                    // TODO(230875908): Properly notify BatteryStats when apps change from active to
+                    // idle, and vice versa
                     StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj;
                     informListeners(r.packageName, r.userId, r.bucket, r.reason,
                             r.isUserInteraction);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 4681d49..d6a067d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -919,7 +919,7 @@
     field public int id;
     field public String lastLoggedInFingerprint;
     field public long lastLoggedInTime;
-    field public String name;
+    field @Nullable public String name;
     field public boolean partial;
     field public boolean preCreated;
     field public int profileBadge;
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 974d20a..e820733 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -7770,10 +7770,11 @@
      * user will always see the normal notification view.
      *
      * <p>
-     * If the app is targeting Android P and above, it is required to use the {@link Person}
-     * class in order to get an optimal rendering of the notification and its avatars. For
-     * conversations involving multiple people, the app should also make sure that it marks the
-     * conversation as a group with {@link #setGroupConversation(boolean)}.
+     * If the app is targeting Android {@link android.os.Build.VERSION_CODES#P} and above, it is
+     * required to use the {@link Person} class in order to get an optimal rendering of the
+     * notification and its avatars. For conversations involving multiple people, the app should
+     * also make sure that it marks the conversation as a group with
+     * {@link #setGroupConversation(boolean)}.
      *
      * <p>
      * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior.
@@ -7846,8 +7847,8 @@
          * @param user Required - The person displayed for any messages that are sent by the
          * user. Any messages added with {@link #addMessage(Notification.MessagingStyle.Message)}
          * who don't have a Person associated with it will be displayed as if they were sent
-         * by this user. The user also needs to have a valid name associated with it, which will
-         * be enforced starting in Android P.
+         * by this user. The user also needs to have a valid name associated with it, which is
+         * enforced starting in Android {@link android.os.Build.VERSION_CODES#P}.
          */
         public MessagingStyle(@NonNull Person user) {
             mUser = user;
@@ -7904,9 +7905,9 @@
         /**
          * Sets the title to be displayed on this conversation. May be set to {@code null}.
          *
-         * <p>Starting in {@link Build.VERSION_CODES#R, this conversation title will be ignored if a
-         * valid shortcutId is added via {@link Notification.Builder#setShortcutId(String)}. In this
-         * case, {@link ShortcutInfo#getLongLabel()} (or, if missing,
+         * <p>Starting in {@link Build.VERSION_CODES#R}, this conversation title will be ignored
+         * if a valid shortcutId is added via {@link Notification.Builder#setShortcutId(String)}.
+         * In this case, {@link ShortcutInfo#getLongLabel()} (or, if missing,
          * {@link ShortcutInfo#getShortLabel()}) will be shown as the conversation title
          * instead.
          *
@@ -8065,7 +8066,7 @@
         }
 
         /**
-         * Gets the list of {@code Message} objects that represent the notification
+         * Gets the list of {@code Message} objects that represent the notification.
          */
         public List<Message> getMessages() {
             return mMessages;
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index a8ae191..32207af 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -110,19 +110,6 @@
             "options": [
                 {
                     "exclude-annotation": "androidx.test.filters.FlakyTest"
-                },
-                // TODO(b/225076204): Remove the following four test cases after fixing the test fail.
-                {
-                    "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_createDetectorTwiceQuickly_triggerSuccess"
-                },
-                {
-                    "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_onDetectFromExternalSource_success"
-                },
-                {
-                    "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_createDetectorTwiceQuickly_triggerSuccess"
-                },
-                {
-                    "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_onDetectFromMic_success"
                 }
             ],
             "file_patterns": ["(/|^)VoiceInteract[^/]*"]
diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java
index ea30ef7..c8033fa 100644
--- a/core/java/android/app/admin/DevicePolicyResources.java
+++ b/core/java/android/app/admin/DevicePolicyResources.java
@@ -863,12 +863,6 @@
                     PREFIX + "REMOVE_AND_UNINSTALL_DEVICE_ADMIN";
 
             /**
-             * Title for selecting device admin apps
-             */
-            public static final String SELECT_DEVICE_ADMIN_APPS =
-                    PREFIX + "SELECT_DEVICE_ADMIN_APPS";
-
-            /**
              * Message when there are no available device admin apps to display
              */
             public static final String NO_DEVICE_ADMINS = PREFIX + "NO_DEVICE_ADMINS";
@@ -910,11 +904,6 @@
                     PREFIX + "ACTIVE_DEVICE_ADMIN_WARNING";
 
             /**
-             * Title for screen to set a profile owner
-             */
-            public static final String SET_PROFILE_OWNER_TITLE = PREFIX + "SET_PROFILE_OWNER_TITLE";
-
-            /**
              * Simplified title for dialog to set a profile owner
              */
             public static final String SET_PROFILE_OWNER_DIALOG_TITLE =
@@ -1173,7 +1162,93 @@
             /**
              * Header for items under the personal user
              */
-            public static final String PERSONAL_CATEGORY_HEADER = PREFIX + "category_personal";
+            public static final String PERSONAL_CATEGORY_HEADER =
+                    PREFIX + "PERSONAL_CATEGORY_HEADER";
+
+            /**
+             * Text to indicate work notification content will be shown on the lockscreen.
+             */
+            public static final String LOCK_SCREEN_SHOW_WORK_NOTIFICATION_CONTENT =
+                    PREFIX + "LOCK_SCREEN_SHOW_WORK_NOTIFICATION_CONTENT";
+
+            /**
+             * Text to indicate work notification content will be shown on the lockscreen.
+             */
+            public static final String LOCK_SCREEN_HIDE_WORK_NOTIFICATION_CONTENT =
+                    PREFIX + "LOCK_SCREEN_HIDE_WORK_NOTIFICATION_CONTENT";
+
+            /**
+             * Text for toggle to enable auto-sycing personal data
+             */
+            public static final String AUTO_SYNC_PERSONAL_DATA = PREFIX
+                    + "AUTO_SYNC_PERSONAL_DATA";
+
+            /**
+             * Text for toggle to enable auto-sycing work data
+             */
+            public static final String AUTO_SYNC_WORK_DATA = PREFIX + "AUTO_SYNC_WORK_DATA";
+
+            /**
+             * Summary for "More security settings" section when a work profile is on the device.
+             */
+            public static final String MORE_SECURITY_SETTINGS_WORK_PROFILE_SUMMARY = PREFIX
+                    + "MORE_SECURITY_SETTINGS_WORK_PROFILE_SUMMARY";
+
+            /**
+             * Title for screen asking the user to choose a type of screen lock (such as a pattern,
+             * PIN, or password) that they need to enter to use their work apps
+             */
+            public static final String LOCK_SETTINGS_NEW_PROFILE_LOCK_TITLE = PREFIX
+                    + "LOCK_SETTINGS_NEW_PROFILE_LOCK_TITLE";
+
+            /**
+             * Title for screen asking the user to update the type of screen lock (such as a
+             * pattern, PIN, or password) that they need to enter to use their work apps
+             */
+            public static final String LOCK_SETTINGS_UPDATE_PROFILE_LOCK_TITLE = PREFIX
+                    + "LOCK_SETTINGS_UPDATE_PROFILE_LOCK_TITLE";
+
+            /**
+             * Title for section listing information that can be seen by organization
+             */
+            public static final String INFORMATION_SEEN_BY_ORGANIZATION_TITLE = PREFIX
+                    + "INFORMATION_SEEN_BY_ORGANIZATION_TITLE";
+
+            /**
+             * Title for section listing changes made by the organization.
+             */
+            public static final String CHANGES_BY_ORGANIZATION_TITLE =
+                    PREFIX + "CHANGES_BY_ORGANIZATION_TITLE";
+
+            /**
+             * Footer for enterprise privacy screen.
+             */
+            public static final String ENTERPRISE_PRIVACY_FOOTER =
+                    PREFIX + "ENTERPRISE_PRIVACY_FOOTER";
+
+            /**
+             * Title for spell checker settings for work.
+             */
+            public static final String SPELL_CHECKER_FOR_WORK =
+                    PREFIX + "SPELL_CHECKER_FOR_WORK";
+
+            /**
+             * Title for personal dictionary for work settings.
+             */
+            public static final String PERSONAL_DICTIONARY_FOR_WORK =
+                    PREFIX + "PERSONAL_DICTIONARY_FOR_WORK";
+
+            /**
+             * Summary for switch preference to indicate it is disabled by the admin
+             */
+            public static final String DISABLED_BY_ADMIN_SWITCH_SUMMARY =
+                    PREFIX + "DISABLED_BY_ADMIN_SWITCH_SUMMARY";
+
+            /**
+             * Summary for switch preference to indicate it is enabled by the admin
+             */
+            public static final String ENABLED_BY_ADMIN_SWITCH_SUMMARY =
+                    PREFIX + "ENABLED_BY_ADMIN_SWITCH_SUMMARY";
         }
 
         /**
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 4d56c1d..907db7d 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -513,10 +513,23 @@
      * restart.  There is no guarantee this will be respected, as the system
      * tries to balance such requests from one app vs. the importance of
      * keeping other apps around.
+     *
+     * @deprecated Repurposed to {@link #BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE}.
      */
+    @Deprecated
     public static final int BIND_VISIBLE = 0x10000000;
 
     /**
+     * @hide Flag for {@link #bindService}: Treat the binding as hosting a foreground service
+     * and also visible to the user. That is, the app hosting the service will get its process state
+     * bumped to the {@link android.app.ActivityManager#PROCESS_STATE_FOREGROUND_SERVICE},
+     * and it's considered as visible to the user, thus less likely to be expunged from memory
+     * on low memory situations. This is intented for use by processes with the process state
+     * better than the {@link android.app.ActivityManager#PROCESS_STATE_TOP}.
+     */
+    public static final int BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE = 0x10000000;
+
+    /**
      * @hide
      * Flag for {@link #bindService}: Consider this binding to be causing the target
      * process to be showing UI, so it will be do a UI_HIDDEN memory trim when it goes
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 76e9fcb..d6e13ac 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -170,7 +171,7 @@
     @UnsupportedAppUsage
     public int serialNumber;
     @UnsupportedAppUsage
-    public String name;
+    public @Nullable String name;
     @UnsupportedAppUsage
     public String iconPath;
     @UnsupportedAppUsage
diff --git a/core/java/android/hardware/ISensorPrivacyManager.aidl b/core/java/android/hardware/ISensorPrivacyManager.aidl
index a392afd..9cf329c 100644
--- a/core/java/android/hardware/ISensorPrivacyManager.aidl
+++ b/core/java/android/hardware/ISensorPrivacyManager.aidl
@@ -50,5 +50,7 @@
     void suppressToggleSensorPrivacyReminders(int userId, int sensor, IBinder token,
             boolean suppress);
 
+    boolean requiresAuthentication();
+
     void showSensorUseDialog(int sensor);
 }
\ No newline at end of file
diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java
index 0460e58..99b58c9 100644
--- a/core/java/android/hardware/SensorPrivacyManager.java
+++ b/core/java/android/hardware/SensorPrivacyManager.java
@@ -327,6 +327,8 @@
     @NonNull
     private boolean mToggleListenerRegistered = false;
 
+    private Boolean mRequiresAuthentication = null;
+
     /**
      * Private constructor to ensure only a single instance is created.
      */
@@ -761,6 +763,23 @@
     }
 
     /**
+     * @return whether the device is required to be unlocked to change software state.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
+    public boolean requiresAuthentication() {
+        if (mRequiresAuthentication == null) {
+            try {
+                mRequiresAuthentication = mService.requiresAuthentication();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return mRequiresAuthentication;
+    }
+
+    /**
      * If sensor privacy for the provided sensor is enabled then this call will show the user the
      * dialog which is shown when an application attempts to use that sensor. If privacy isn't
      * enabled then this does nothing.
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index b05e6d1..a90eb88 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1058,7 +1058,7 @@
      * must select one unique size from this metadata to use (e.g., preview and recording streams
      * must have the same size). Otherwise, the high speed capture session creation will fail.</p>
      * <p>The min and max fps will be multiple times of 30fps.</p>
-     * <p>High speed video streaming extends significant performance pressue to camera hardware,
+     * <p>High speed video streaming extends significant performance pressure to camera hardware,
      * to achieve efficient high speed streaming, the camera device may have to aggregate
      * multiple frames together and send to camera device for processing where the request
      * controls are same for all the frames in this batch. Max batch size indicates
@@ -1143,7 +1143,7 @@
      * <p>Range of boosts for {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost} supported
      * by this camera device.</p>
      * <p>Devices support post RAW sensitivity boost  will advertise
-     * {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost} key for controling
+     * {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost} key for controlling
      * post RAW sensitivity boost.</p>
      * <p>This key will be <code>null</code> for devices that do not support any RAW format
      * outputs. For devices that do support RAW format outputs, this key will always
@@ -1323,7 +1323,7 @@
      * <p>Maximum flashlight brightness level.</p>
      * <p>If this value is greater than 1, then the device supports controlling the
      * flashlight brightness level via
-     * {android.hardware.camera2.CameraManager#turnOnTorchWithStrengthLevel}.
+     * {@link android.hardware.camera2.CameraManager#turnOnTorchWithStrengthLevel }.
      * If this value is equal to 1, flashlight brightness control is not supported.
      * The value for this key will be null for devices with no flash unit.</p>
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
@@ -1335,7 +1335,7 @@
 
     /**
      * <p>Default flashlight brightness level to be set via
-     * {android.hardware.camera2.CameraManager#turnOnTorchWithStrengthLevel}.</p>
+     * {@link android.hardware.camera2.CameraManager#turnOnTorchWithStrengthLevel }.</p>
      * <p>If flash unit is available this will be greater than or equal to 1 and less
      * or equal to <code>{@link CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL android.flash.info.strengthMaximumLevel}</code>.</p>
      * <p>Setting flashlight brightness above the default level
@@ -1376,7 +1376,7 @@
      * camera device.</p>
      * <p>This list will include at least one non-zero resolution, plus <code>(0,0)</code> for indicating no
      * thumbnail should be generated.</p>
-     * <p>Below condiditions will be satisfied for this size list:</p>
+     * <p>Below conditions will be satisfied for this size list:</p>
      * <ul>
      * <li>The sizes will be sorted by increasing pixel area (width x height).
      * If several resolutions have the same area, they will be sorted by increasing width.</li>
@@ -1982,7 +1982,7 @@
      * the camera device. Using more streams simultaneously may require more hardware and
      * CPU resources that will consume more power. The image format for an output stream can
      * be any supported format provided by android.scaler.availableStreamConfigurations.
-     * The formats defined in android.scaler.availableStreamConfigurations can be catergorized
+     * The formats defined in android.scaler.availableStreamConfigurations can be categorized
      * into the 3 stream types as below:</p>
      * <ul>
      * <li>Processed (but stalling): any non-RAW format with a stallDurations &gt; 0.
@@ -2324,7 +2324,7 @@
      * but clients should be aware and expect delays during their application.
      * An example usage scenario could look like this:</p>
      * <ul>
-     * <li>The camera client starts by quering the session parameter key list via
+     * <li>The camera client starts by querying the session parameter key list via
      *   {@link android.hardware.camera2.CameraCharacteristics#getAvailableSessionKeys }.</li>
      * <li>Before triggering the capture session create sequence, a capture request
      *   must be built via
@@ -2379,7 +2379,7 @@
      * {@link android.hardware.camera2.CameraCharacteristics#getKeys } that require camera clients
      * to acquire the {@link android.Manifest.permission#CAMERA } permission before calling
      * {@link android.hardware.camera2.CameraManager#getCameraCharacteristics }. If the
-     * permission is not held by the camera client, then the values of the repsective properties
+     * permission is not held by the camera client, then the values of the respective properties
      * will not be present in {@link android.hardware.camera2.CameraCharacteristics }.</p>
      * <p>This key is available on all devices.</p>
      * @hide
@@ -2759,7 +2759,7 @@
      * </table>
      * <p>For applications targeting SDK version 31 or newer, if the mobile device declares to be
      * media performance class 12 or higher by setting
-     * {@link android.os.Build.VERSION_CDOES.MEDIA_PERFORMANCE_CLASS } to be 31 or larger,
+     * {@link android.os.Build.VERSION_CODES.MEDIA_PERFORMANCE_CLASS } to be 31 or larger,
      * the primary camera devices (first rear/front camera in the camera ID list) will not
      * support JPEG sizes smaller than 1080p. If the application configures a JPEG stream
      * smaller than 1080p, the camera device will round up the JPEG image size to at least
@@ -2833,7 +2833,7 @@
      * </table>
      * <p>For applications targeting SDK version 31 or newer, if the mobile device doesn't declare
      * to be media performance class 12 or better by setting
-     * {@link android.os.Build.VERSION_CDOES.MEDIA_PERFORMANCE_CLASS } to be 31 or larger,
+     * {@link android.os.Build.VERSION_CODES.MEDIA_PERFORMANCE_CLASS } to be 31 or larger,
      * or if the camera device isn't a primary rear/front camera, the minimum required output
      * stream configurations are the same as for applications targeting SDK version older than
      * 31.</p>
@@ -3485,14 +3485,16 @@
      * to output different resolution images depending on the current active physical camera or
      * pixel mode. With multi-resolution input streams, the camera device can reprocess images
      * of different resolutions from different physical cameras or sensor pixel modes.</p>
-     * <p>When set to TRUE:
-     * * For a logical multi-camera, the camera framework derives
+     * <p>When set to TRUE:</p>
+     * <ul>
+     * <li>For a logical multi-camera, the camera framework derives
      * {@link CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP android.scaler.multiResolutionStreamConfigurationMap} by combining the
      * android.scaler.physicalCameraMultiResolutionStreamConfigurations from its physical
-     * cameras.
-     * * For an ultra-high resolution sensor camera, the camera framework directly copies
+     * cameras.</li>
+     * <li>For an ultra-high resolution sensor camera, the camera framework directly copies
      * the value of android.scaler.physicalCameraMultiResolutionStreamConfigurations to
-     * {@link CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP android.scaler.multiResolutionStreamConfigurationMap}.</p>
+     * {@link CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP android.scaler.multiResolutionStreamConfigurationMap}.</li>
+     * </ul>
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
      * <p><b>Limited capability</b> -
      * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
@@ -3513,7 +3515,7 @@
      * capture, video record for encoding the camera output for the purpose of future playback,
      * and video call for live realtime video conferencing.</p>
      * <p>With this flag, the camera device can optimize the image processing pipeline
-     * parameters, such as tuning, sensor mode, and ISP settings, indepedent of
+     * parameters, such as tuning, sensor mode, and ISP settings, independent of
      * the properties of the immediate camera output surface. For example, if the output
      * surface is a SurfaceTexture, the stream use case flag can be used to indicate whether
      * the camera frames eventually go to display, video encoder,
@@ -3535,7 +3537,7 @@
      * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE }
      * capability is documented in the camera device
      * {@link android.hardware.camera2.CameraDevice#createCaptureSession guideline}. The
-     * application is strongly recommended to use one of the guaranteed stream combintations.
+     * application is strongly recommended to use one of the guaranteed stream combinations.
      * If the application creates a session with a stream combination not in the guaranteed
      * list, or with mixed DEFAULT and non-DEFAULT use cases within the same session,
      * the camera device may ignore some stream use cases due to hardware constraints
@@ -5209,7 +5211,7 @@
      * EXTERIOR_* value.</p>
      * <p>If a camera has INTERIOR_OTHER or EXTERIOR_OTHER, or more than one camera is at the
      * same location and facing the same direction, their static metadata will list the
-     * following entries, so that applications can determain their lenses' exact facing
+     * following entries, so that applications can determine their lenses' exact facing
      * directions:</p>
      * <ul>
      * <li>{@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference}</li>
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 40565b0..eb8c73a 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -409,7 +409,7 @@
 
     /**
      * <p>The value of {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation} is relative to the origin of the
-     * automotive sensor coodinate system, which is at the center of the rear axle.</p>
+     * automotive sensor coordinate system, which is at the center of the rear axle.</p>
      *
      * @see CameraCharacteristics#LENS_POSE_TRANSLATION
      * @see CameraCharacteristics#LENS_POSE_REFERENCE
@@ -683,7 +683,7 @@
      * captured at the same rate as the maximum-size YUV_420_888 resolution is.</p>
      * <p>If the device supports the PRIVATE_REPROCESSING capability, then the same guarantees
      * as for the YUV_420_888 format also apply to the {@link android.graphics.ImageFormat#PRIVATE } format.</p>
-     * <p>In addition, the {@link CameraCharacteristics#SYNC_MAX_LATENCY android.sync.maxLatency} field is guaranted to have a value between 0
+     * <p>In addition, the {@link CameraCharacteristics#SYNC_MAX_LATENCY android.sync.maxLatency} field is guaranteed to have a value between 0
      * and 4, inclusive. {@link CameraCharacteristics#CONTROL_AE_LOCK_AVAILABLE android.control.aeLockAvailable} and {@link CameraCharacteristics#CONTROL_AWB_LOCK_AVAILABLE android.control.awbLockAvailable}
      * are also guaranteed to be <code>true</code> so burst capture with these two locks ON yields
      * consistent image output.</p>
@@ -843,7 +843,7 @@
      * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoSizes }.</li>
      * <li>The FPS ranges are selected from {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoFpsRanges }.</li>
      * </ul>
-     * <p>When above conditions are NOT satistied,
+     * <p>When above conditions are NOT satisfied,
      * {@link android.hardware.camera2.CameraDevice#createConstrainedHighSpeedCaptureSession }
      * will fail.</p>
      * <p>Switching to a FPS range that has different maximum FPS may trigger some camera device
@@ -986,7 +986,7 @@
      * non-active physical cameras. For example, if the logical camera has a wide-ultrawide
      * configuration where the wide lens is the default, when the crop region is set to the
      * logical camera's active array size, (and the zoom ratio set to 1.0 starting from
-     * Android 11), a physical stream for the ultrawide camera may prefer outputing images
+     * Android 11), a physical stream for the ultrawide camera may prefer outputting images
      * with larger field-of-view than that of the wide camera for better stereo matching
      * margin or more robust motion tracking. At the same time, the physical non-RAW streams'
      * field of view must not be smaller than the requested crop region and zoom ratio, as
@@ -1175,21 +1175,23 @@
      * when {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
      * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }),
      * the <code>RAW_SENSOR</code> stream will have a regular bayer pattern.</p>
-     * <p>This capability requires the camera device to support the following :
-     * * The {@link android.hardware.camera2.params.StreamConfigurationMap } mentioned below
+     * <p>This capability requires the camera device to support the following :</p>
+     * <ul>
+     * <li>The {@link android.hardware.camera2.params.StreamConfigurationMap } mentioned below
      *   refers to the one, described by
-     *   <code>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION android.scaler.streamConfigurationMapMaximumResolution}</code>.
-     * * One input stream is supported, that is, <code>{@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} == 1</code>.
-     * * {@link android.graphics.ImageFormat#RAW_SENSOR } is supported as an output/input
+     *   <code>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION android.scaler.streamConfigurationMapMaximumResolution}</code>.</li>
+     * <li>One input stream is supported, that is, <code>{@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} == 1</code>.</li>
+     * <li>{@link android.graphics.ImageFormat#RAW_SENSOR } is supported as an output/input
      *   format, that is, {@link android.graphics.ImageFormat#RAW_SENSOR } is included in the
-     *   lists of formats returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats } and {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputFormats }.
-     * * {@link android.hardware.camera2.params.StreamConfigurationMap#getValidOutputFormatsForInput }
-     *   returns non-empty int[] for each supported input format returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats }.
-     * * Each size returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputSizes getInputSizes(ImageFormat.RAW_SENSOR)} is also included in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes getOutputSizes(ImageFormat.RAW_SENSOR)}
-     * * Using {@link android.graphics.ImageFormat#RAW_SENSOR } does not cause a frame rate
-     *   drop relative to the sensor's maximum capture rate (at that resolution).
-     * * No CaptureRequest controls will be applicable when a request has an input target
-     *   with {@link android.graphics.ImageFormat#RAW_SENSOR } format.</p>
+     *   lists of formats returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats } and {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputFormats }.</li>
+     * <li>{@link android.hardware.camera2.params.StreamConfigurationMap#getValidOutputFormatsForInput }
+     *   returns non-empty int[] for each supported input format returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats }.</li>
+     * <li>Each size returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputSizes getInputSizes(ImageFormat.RAW_SENSOR)} is also included in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes getOutputSizes(ImageFormat.RAW_SENSOR)}</li>
+     * <li>Using {@link android.graphics.ImageFormat#RAW_SENSOR } does not cause a frame rate
+     *   drop relative to the sensor's maximum capture rate (at that resolution).</li>
+     * <li>No CaptureRequest controls will be applicable when a request has an input target
+     *   with {@link android.graphics.ImageFormat#RAW_SENSOR } format.</li>
+     * </ul>
      *
      * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS
      * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
@@ -1205,16 +1207,18 @@
      * {@link android.hardware.camera2.params.DynamicRangeProfiles#getSupportedProfiles }.
      * They can be configured as part of the capture session initialization via
      * {@link android.hardware.camera2.params.OutputConfiguration#setDynamicRangeProfile }.
-     * Cameras that enable this capability must also support the following:
-     * * Profile {@link android.hardware.camera2.params.DynamicRangeProfiles#HLG10 }
-     * * All mandatory stream combinations for this specific capability as per
-     *   documentation {@link android.hardware.camera2.CameraDevice#createCaptureSession }
-     * * In case the device is not able to capture some combination of supported
+     * Cameras that enable this capability must also support the following:</p>
+     * <ul>
+     * <li>Profile {@link android.hardware.camera2.params.DynamicRangeProfiles#HLG10 }</li>
+     * <li>All mandatory stream combinations for this specific capability as per
+     *   documentation {@link android.hardware.camera2.CameraDevice#createCaptureSession }</li>
+     * <li>In case the device is not able to capture some combination of supported
      *   standard 8-bit and/or 10-bit dynamic range profiles within the same capture request,
      *   then those constraints must be listed in
-     *   {@link android.hardware.camera2.params.DynamicRangeProfiles#getProfileCaptureRequestConstraints }
-     * * Recommended dynamic range profile listed in
-     *   {@link android.hardware.camera2.CameraCharacteristics#REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE }.</p>
+     *   {@link android.hardware.camera2.params.DynamicRangeProfiles#getProfileCaptureRequestConstraints }</li>
+     * <li>Recommended dynamic range profile listed in
+     *   {@link android.hardware.camera2.CameraCharacteristics#REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE }.</li>
+     * </ul>
      * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
      */
     public static final int REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT = 18;
@@ -1224,22 +1228,26 @@
      * {@link android.hardware.camera2.params.OutputConfiguration#setStreamUseCase }
      * so that the device can optimize camera pipeline parameters such as tuning, sensor
      * mode, or ISP settings for a specific user scenario.
-     * Some sample usages of this capability are:
-     * * Distinguish high quality YUV captures from a regular YUV stream where
-     *   the image quality may not be as good as the JPEG stream, or
-     * * Use one stream to serve multiple purposes: viewfinder, video recording and
+     * Some sample usages of this capability are:</p>
+     * <ul>
+     * <li>Distinguish high quality YUV captures from a regular YUV stream where
+     *   the image quality may not be as good as the JPEG stream, or</li>
+     * <li>Use one stream to serve multiple purposes: viewfinder, video recording and
      *   still capture. This is common with applications that wish to apply edits equally
-     *   to preview, saved images, and saved videos.</p>
+     *   to preview, saved images, and saved videos.</li>
+     * </ul>
      * <p>This capability requires the camera device to support the following
-     * stream use cases:
-     * * DEFAULT for backward compatibility where the application doesn't set
-     *   a stream use case
-     * * PREVIEW for live viewfinder and in-app image analysis
-     * * STILL_CAPTURE for still photo capture
-     * * VIDEO_RECORD for recording video clips
-     * * PREVIEW_VIDEO_STILL for one single stream used for viewfinder, video
-     *   recording, and still capture.
-     * * VIDEO_CALL for long running video calls</p>
+     * stream use cases:</p>
+     * <ul>
+     * <li>DEFAULT for backward compatibility where the application doesn't set
+     *   a stream use case</li>
+     * <li>PREVIEW for live viewfinder and in-app image analysis</li>
+     * <li>STILL_CAPTURE for still photo capture</li>
+     * <li>VIDEO_RECORD for recording video clips</li>
+     * <li>PREVIEW_VIDEO_STILL for one single stream used for viewfinder, video
+     *   recording, and still capture.</li>
+     * <li>VIDEO_CALL for long running video calls</li>
+     * </ul>
      * <p>{@link android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES }
      * lists all of the supported stream use cases.</p>
      * <p>Refer to {@link android.hardware.camera2.CameraDevice#createCaptureSession } for the
@@ -1391,10 +1399,10 @@
      * <p>Live stream shown to the user.</p>
      * <p>Optimized for performance and usability as a viewfinder, but not necessarily for
      * image quality. The output is not meant to be persisted as saved images or video.</p>
-     * <p>No stall if android.control.<em> are set to FAST; may have stall if android.control.</em>
-     * are set to HIGH_QUALITY. This use case has the same behavior as the default
-     * SurfaceView and SurfaceTexture targets. Additionally, this use case can be used for
-     * in-app image analysis.</p>
+     * <p>No stall if android.control.* are set to FAST. There may be stall if
+     * they are set to HIGH_QUALITY. This use case has the same behavior as the
+     * default SurfaceView and SurfaceTexture targets. Additionally, this use case can be
+     * used for in-app image analysis.</p>
      * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
      */
     public static final int SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW = 0x1;
@@ -1441,7 +1449,7 @@
     public static final int SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL = 0x4;
 
     /**
-     * <p>Long-running video call optimized for both power efficienty and video quality.</p>
+     * <p>Long-running video call optimized for both power efficiency and video quality.</p>
      * <p>The camera sensor may run in a lower-resolution mode to reduce power consumption
      * at the cost of some image and digital zoom quality. Unlike VIDEO_RECORD, VIDEO_CALL
      * outputs are expected to work in dark conditions, so are usually accompanied with
@@ -2946,10 +2954,10 @@
      * android.control.availableHighSpeedVideoConfigurations.</li>
      * <li>No processed non-stalling or raw streams are configured.</li>
      * </ul>
-     * <p>When above conditions are NOT satistied, the controls of this mode and
+     * <p>When above conditions are NOT satisfied, the controls of this mode and
      * {@link CaptureRequest#CONTROL_AE_TARGET_FPS_RANGE android.control.aeTargetFpsRange} will be ignored by the camera device,
      * the camera device will fall back to {@link CaptureRequest#CONTROL_MODE android.control.mode} <code>==</code> AUTO,
-     * and the returned capture result metadata will give the fps range choosen
+     * and the returned capture result metadata will give the fps range chosen
      * by the camera device.</p>
      * <p>Switching into or out of this mode may trigger some camera ISP/sensor
      * reconfigurations, which may introduce extra latency. It is recommended that
@@ -3034,7 +3042,7 @@
      * if the {@link CameraCharacteristics#SENSOR_INFO_SENSITIVITY_RANGE android.sensor.info.sensitivityRange} gives range of [100, 1600],
      * the camera device auto-exposure routine tuning process may limit the actual
      * exposure sensitivity range to [100, 1200] to ensure that the noise level isn't
-     * exessive in order to preserve the image quality. Under this situation, the image under
+     * excessive in order to preserve the image quality. Under this situation, the image under
      * low light may be under-exposed when the sensor max exposure time (bounded by the
      * {@link CaptureRequest#CONTROL_AE_TARGET_FPS_RANGE android.control.aeTargetFpsRange} when {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is one of the
      * ON_* modes) and effective max sensitivity are reached. This scene mode allows the
@@ -3631,7 +3639,7 @@
     public static final int TONEMAP_MODE_HIGH_QUALITY = 2;
 
     /**
-     * <p>Use the gamma value specified in {@link CaptureRequest#TONEMAP_GAMMA android.tonemap.gamma} to peform
+     * <p>Use the gamma value specified in {@link CaptureRequest#TONEMAP_GAMMA android.tonemap.gamma} to perform
      * tonemapping.</p>
      * <p>All color enhancement and tonemapping must be disabled, except
      * for applying the tonemapping curve specified by {@link CaptureRequest#TONEMAP_GAMMA android.tonemap.gamma}.</p>
@@ -3644,7 +3652,7 @@
 
     /**
      * <p>Use the preset tonemapping curve specified in
-     * {@link CaptureRequest#TONEMAP_PRESET_CURVE android.tonemap.presetCurve} to peform tonemapping.</p>
+     * {@link CaptureRequest#TONEMAP_PRESET_CURVE android.tonemap.presetCurve} to perform tonemapping.</p>
      * <p>All color enhancement and tonemapping must be disabled, except
      * for applying the tonemapping curve specified by
      * {@link CaptureRequest#TONEMAP_PRESET_CURVE android.tonemap.presetCurve}.</p>
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 15e59e0..c5cf0f6 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -1767,7 +1767,7 @@
      * routine is enabled, overriding the application's selected
      * {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}, {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} and
      * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}. Note that when {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode}
-     * is OFF, the behavior of AWB is device dependent. It is recommened to
+     * is OFF, the behavior of AWB is device dependent. It is recommended to
      * also set AWB mode to OFF or lock AWB by using {@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} before
      * setting AE mode to OFF.</p>
      * <p>When set to the OFF mode, the camera device's auto-white balance
@@ -1917,13 +1917,15 @@
      * strategy.</p>
      * <p>This control (except for MANUAL) is only effective if
      * <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p>
-     * <p>All intents are supported by all devices, except that:
-     *   * ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
-     * PRIVATE_REPROCESSING or YUV_REPROCESSING.
-     *   * MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
-     * MANUAL_SENSOR.
-     *   * MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
-     * MOTION_TRACKING.</p>
+     * <p>All intents are supported by all devices, except that:</p>
+     * <ul>
+     * <li>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+     * PRIVATE_REPROCESSING or YUV_REPROCESSING.</li>
+     * <li>MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+     * MANUAL_SENSOR.</li>
+     * <li>MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+     * MOTION_TRACKING.</li>
+     * </ul>
      * <p><b>Possible values:</b></p>
      * <ul>
      *   <li>{@link #CONTROL_CAPTURE_INTENT_CUSTOM CUSTOM}</li>
@@ -2680,7 +2682,7 @@
      *   and keep jpeg and thumbnail image data unrotated.</li>
      * <li>Rotate the jpeg and thumbnail image data and not set
      *   {@link android.media.ExifInterface#TAG_ORIENTATION EXIF orientation flag}. In this
-     *   case, LIMITED or FULL hardware level devices will report rotated thumnail size in
+     *   case, LIMITED or FULL hardware level devices will report rotated thumbnail size in
      *   capture result, so the width and height will be interchanged if 90 or 270 degree
      *   orientation is requested. LEGACY device will always report unrotated thumbnail
      *   size.</li>
@@ -3806,9 +3808,11 @@
     /**
      * <p>Tonemapping curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is
      * GAMMA_VALUE</p>
-     * <p>The tonemap curve will be defined the following formula:
-     * * OUT = pow(IN, 1.0 / gamma)
-     * where IN and OUT is the input pixel value scaled to range [0.0, 1.0],
+     * <p>The tonemap curve will be defined the following formula:</p>
+     * <ul>
+     * <li>OUT = pow(IN, 1.0 / gamma)</li>
+     * </ul>
+     * <p>where IN and OUT is the input pixel value scaled to range [0.0, 1.0],
      * pow is the power function and gamma is the gamma value specified by this
      * key.</p>
      * <p>The same curve will be applied to all color channels. The camera device
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 1faec5b..3e1deb2 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -1173,7 +1173,7 @@
      * <td align="center">Any state (excluding LOCKED)</td>
      * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td>
      * <td align="center">CONVERGED</td>
-     * <td align="center">Converged after a precapture sequenceis canceled, transient states are skipped by camera device.</td>
+     * <td align="center">Converged after a precapture sequences canceled, transient states are skipped by camera device.</td>
      * </tr>
      * <tr>
      * <td align="center">CONVERGED</td>
@@ -1847,7 +1847,7 @@
      * routine is enabled, overriding the application's selected
      * {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}, {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} and
      * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}. Note that when {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode}
-     * is OFF, the behavior of AWB is device dependent. It is recommened to
+     * is OFF, the behavior of AWB is device dependent. It is recommended to
      * also set AWB mode to OFF or lock AWB by using {@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} before
      * setting AE mode to OFF.</p>
      * <p>When set to the OFF mode, the camera device's auto-white balance
@@ -1997,13 +1997,15 @@
      * strategy.</p>
      * <p>This control (except for MANUAL) is only effective if
      * <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p>
-     * <p>All intents are supported by all devices, except that:
-     *   * ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
-     * PRIVATE_REPROCESSING or YUV_REPROCESSING.
-     *   * MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
-     * MANUAL_SENSOR.
-     *   * MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
-     * MOTION_TRACKING.</p>
+     * <p>All intents are supported by all devices, except that:</p>
+     * <ul>
+     * <li>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+     * PRIVATE_REPROCESSING or YUV_REPROCESSING.</li>
+     * <li>MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+     * MANUAL_SENSOR.</li>
+     * <li>MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+     * MOTION_TRACKING.</li>
+     * </ul>
      * <p><b>Possible values:</b></p>
      * <ul>
      *   <li>{@link #CONTROL_CAPTURE_INTENT_CUSTOM CUSTOM}</li>
@@ -2929,7 +2931,7 @@
      *   and keep jpeg and thumbnail image data unrotated.</li>
      * <li>Rotate the jpeg and thumbnail image data and not set
      *   {@link android.media.ExifInterface#TAG_ORIENTATION EXIF orientation flag}. In this
-     *   case, LIMITED or FULL hardware level devices will report rotated thumnail size in
+     *   case, LIMITED or FULL hardware level devices will report rotated thumbnail size in
      *   capture result, so the width and height will be interchanged if 90 or 270 degree
      *   orientation is requested. LEGACY device will always report unrotated thumbnail
      *   size.</li>
@@ -3149,7 +3151,7 @@
      * <p>When the state is STATIONARY, the lens parameters are not changing. This could be
      * either because the parameters are all fixed, or because the lens has had enough
      * time to reach the most recently-requested values.
-     * If all these lens parameters are not changable for a camera device, as listed below:</p>
+     * If all these lens parameters are not changeable for a camera device, as listed below:</p>
      * <ul>
      * <li>Fixed focus (<code>{@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance} == 0</code>), which means
      * {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance} parameter will always be 0.</li>
@@ -4009,7 +4011,7 @@
      * noise model used here is:</p>
      * <p>N(x) = sqrt(Sx + O)</p>
      * <p>Where x represents the recorded signal of a CFA channel normalized to
-     * the range [0, 1], and S and O are the noise model coeffiecients for
+     * the range [0, 1], and S and O are the noise model coefficients for
      * that channel.</p>
      * <p>A more detailed description of the noise model can be found in the
      * Adobe DNG specification for the NoiseProfile tag.</p>
@@ -4054,7 +4056,7 @@
      * <li>1.20 &lt;= R &gt;= 1.03 will require some software
      * correction to avoid demosaic errors (3-20% divergence).</li>
      * <li>R &gt; 1.20 will require strong software correction to produce
-     * a usuable image (&gt;20% divergence).</li>
+     * a usable image (&gt;20% divergence).</li>
      * </ul>
      * <p>Starting from Android Q, this key will not be present for a MONOCHROME camera, even if
      * the camera device has RAW capability.</p>
@@ -4274,7 +4276,7 @@
     /**
      * <p>Whether <code>RAW</code> images requested have their bayer pattern as described by
      * {@link CameraCharacteristics#SENSOR_INFO_BINNING_FACTOR android.sensor.info.binningFactor}.</p>
-     * <p>This key will only be present in devices advertisting the
+     * <p>This key will only be present in devices advertising the
      * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR }
      * capability which also advertise <code>REMOSAIC_REPROCESSING</code> capability. On all other devices
      * RAW targets will have a regular bayer pattern.</p>
@@ -5128,9 +5130,11 @@
     /**
      * <p>Tonemapping curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is
      * GAMMA_VALUE</p>
-     * <p>The tonemap curve will be defined the following formula:
-     * * OUT = pow(IN, 1.0 / gamma)
-     * where IN and OUT is the input pixel value scaled to range [0.0, 1.0],
+     * <p>The tonemap curve will be defined the following formula:</p>
+     * <ul>
+     * <li>OUT = pow(IN, 1.0 / gamma)</li>
+     * </ul>
+     * <p>where IN and OUT is the input pixel value scaled to range [0.0, 1.0],
      * pow is the power function and gamma is the gamma value specified by this
      * key.</p>
      * <p>The same curve will be applied to all color channels. The camera device
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 1263da6..4d0ba63 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -72,7 +72,7 @@
 import java.util.concurrent.Executor;
 
 public final class CameraExtensionSessionImpl extends CameraExtensionSession {
-    private static final int PREVIEW_QUEUE_SIZE = 3;
+    private static final int PREVIEW_QUEUE_SIZE = 10;
     private static final String TAG = "CameraExtensionSessionImpl";
 
     private final Executor mExecutor;
@@ -1057,15 +1057,8 @@
                                                 mClientRequest));
 
                         if (mCaptureResultHandler != null) {
-                            CameraMetadataNative captureResults = new CameraMetadataNative();
-                            for (CaptureResult.Key key : mSupportedResultKeys) {
-                                Object value = result.get(key);
-                                if (value != null) {
-                                    captureResults.set(key, value);
-                                }
-                            }
                             mCaptureResultHandler.onCaptureCompleted(timestamp,
-                                    captureResults);
+                                    initializeFilteredResults(result));
                         }
                     } finally {
                         Binder.restoreCallingIdentity(ident);
@@ -1127,6 +1120,11 @@
 
         private class ImageCallback implements OnImageAvailableListener {
             @Override
+            public void onImageDropped(long timestamp) {
+                notifyCaptureFailed();
+            }
+
+            @Override
             public void onImageAvailable(ImageReader reader, Image img) {
                 if (mCaptureFailed) {
                     img.close();
@@ -1160,6 +1158,9 @@
 
     private class ImageLoopbackCallback implements OnImageAvailableListener {
         @Override
+        public void onImageDropped(long timestamp) { }
+
+        @Override
         public void onImageAvailable(ImageReader reader, Image img) {
             img.close();
         }
@@ -1221,7 +1222,8 @@
     }
 
     private interface OnImageAvailableListener {
-        public void onImageAvailable (ImageReader reader, Image img);
+        void onImageDropped(long timestamp);
+        void onImageAvailable (ImageReader reader, Image img);
     }
 
     private class CameraOutputImageCallback implements ImageReader.OnImageAvailableListener,
@@ -1263,6 +1265,29 @@
             } else {
                 mImageListenerMap.put(img.getTimestamp(), new Pair<>(img, null));
             }
+
+            notifyDroppedImages(timestamp);
+        }
+
+        private void notifyDroppedImages(long timestamp) {
+            Set<Long> timestamps = mImageListenerMap.keySet();
+            ArrayList<Long> removedTs = new ArrayList<>();
+            for (long ts : timestamps) {
+                if (ts < timestamp) {
+                    Log.e(TAG, "Dropped image with ts: " + ts);
+                    Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.get(ts);
+                    if (entry.second != null) {
+                        entry.second.onImageDropped(ts);
+                    }
+                    if (entry.first != null) {
+                        entry.first.close();
+                    }
+                    removedTs.add(ts);
+                }
+            }
+            for (long ts : removedTs) {
+                mImageListenerMap.remove(ts);
+            }
         }
 
         public void registerListener(Long timestamp, OnImageAvailableListener listener) {
@@ -1291,6 +1316,12 @@
                     entry.first.close();
                 }
             }
+            for (long timestamp : mImageListenerMap.keySet()) {
+                Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.get(timestamp);
+                if (entry.second != null) {
+                    entry.second.onImageDropped(timestamp);
+                }
+            }
             mImageListenerMap.clear();
         }
     }
@@ -1447,7 +1478,6 @@
             } else {
                 notifyConfigurationFailure();
             }
-
         }
 
         @Override
@@ -1536,7 +1566,16 @@
                     } else if (mPreviewProcessorType ==
                             IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
                         int idx = mPendingResultMap.indexOfKey(timestamp);
-                        if (idx >= 0) {
+
+                        if ((idx >= 0) && (mPendingResultMap.get(timestamp).first == null)) {
+                            // Image was dropped before we can receive the capture results
+                            if ((mCaptureResultHandler != null)) {
+                                mCaptureResultHandler.onCaptureCompleted(timestamp,
+                                        initializeFilteredResults(result));
+                            }
+                            discardPendingRepeatingResults(idx, mPendingResultMap, false);
+                        } else  if (idx >= 0) {
+                            // Image came before the capture results
                             ParcelImage parcelImage = initializeParcelImage(
                                     mPendingResultMap.get(timestamp).first);
                             try {
@@ -1563,6 +1602,7 @@
                             }
                             discardPendingRepeatingResults(idx, mPendingResultMap, false);
                         } else {
+                            // Image not yet available
                             notifyClient = false;
                             mPendingResultMap.put(timestamp,
                                     new Pair<>(null,
@@ -1581,16 +1621,8 @@
                                                 mClientRequest));
                                 if ((mCaptureResultHandler != null) && (mPreviewProcessorType !=
                                         IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR)) {
-                                    CameraMetadataNative captureResults =
-                                            new CameraMetadataNative();
-                                    for (CaptureResult.Key key : mSupportedResultKeys) {
-                                        Object value = result.get(key);
-                                        if (value != null) {
-                                            captureResults.set(key, value);
-                                        }
-                                    }
                                     mCaptureResultHandler.onCaptureCompleted(timestamp,
-                                            captureResults);
+                                            initializeFilteredResults(result));
                                 }
                             } else {
                                 mExecutor.execute(
@@ -1657,19 +1689,24 @@
             for (int i = idx; i >= 0; i--) {
                 if (previewMap.valueAt(i).first != null) {
                     previewMap.valueAt(i).first.close();
-                } else {
-                    if (mClientNotificationsEnabled && ((i != idx) || notifyCurrentIndex)) {
-                        Log.w(TAG, "Preview frame drop with timestamp: " + previewMap.keyAt(i));
-                        final long ident = Binder.clearCallingIdentity();
-                        try {
-                            mExecutor.execute(
-                                    () -> mCallbacks
-                                            .onCaptureFailed(CameraExtensionSessionImpl.this,
-                                                    mClientRequest));
-                        } finally {
-                            Binder.restoreCallingIdentity(ident);
-                        }
+                } else if (mClientNotificationsEnabled && (previewMap.valueAt(i).second != null) &&
+                        ((i != idx) || notifyCurrentIndex)) {
+                    TotalCaptureResult result = previewMap.valueAt(i).second;
+                    Long timestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
+                    mCaptureResultHandler.onCaptureCompleted(timestamp,
+                            initializeFilteredResults(result));
+
+                    Log.w(TAG, "Preview frame drop with timestamp: " + previewMap.keyAt(i));
+                    final long ident = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(
+                                () -> mCallbacks
+                                        .onCaptureFailed(CameraExtensionSessionImpl.this,
+                                                mClientRequest));
+                    } finally {
+                        Binder.restoreCallingIdentity(ident);
                     }
+
                 }
                 previewMap.removeAt(i);
             }
@@ -1683,6 +1720,12 @@
             }
 
             @Override
+            public void onImageDropped(long timestamp) {
+                discardPendingRepeatingResults(mPendingResultMap.indexOfKey(timestamp),
+                        mPendingResultMap, true);
+            }
+
+            @Override
             public void onImageAvailable(ImageReader reader, Image img) {
                 if (img == null) {
                     Log.e(TAG, "Invalid image!");
@@ -1703,6 +1746,15 @@
         private class ImageProcessCallback implements OnImageAvailableListener {
 
             @Override
+            public void onImageDropped(long timestamp) {
+                discardPendingRepeatingResults(mPendingResultMap.indexOfKey(timestamp),
+                        mPendingResultMap, true);
+                // Add an empty frame&results entry to flag that we dropped a frame
+                // and valid capture results can immediately return to client.
+                mPendingResultMap.put(timestamp, new Pair<>(null, null));
+            }
+
+            @Override
             public void onImageAvailable(ImageReader reader, Image img) {
                 if (mPendingResultMap.size() + 1 >= PREVIEW_QUEUE_SIZE) {
                     // We reached the maximum acquired images limit. This is possible in case we
@@ -1768,6 +1820,17 @@
         }
     }
 
+    private CameraMetadataNative initializeFilteredResults(TotalCaptureResult result) {
+        CameraMetadataNative captureResults = new CameraMetadataNative();
+        for (CaptureResult.Key key : mSupportedResultKeys) {
+            Object value = result.get(key);
+            if (value != null) {
+                captureResults.set(key, value);
+            }
+        }
+        return captureResults;
+    }
+
     private static Size findSmallestAspectMatchedSize(@NonNull List<Size> sizes,
             @NonNull Size arSize) {
         final float TOLL = .01f;
diff --git a/core/java/android/hardware/camera2/params/DynamicRangeProfiles.java b/core/java/android/hardware/camera2/params/DynamicRangeProfiles.java
index 14ed689..34c8336 100644
--- a/core/java/android/hardware/camera2/params/DynamicRangeProfiles.java
+++ b/core/java/android/hardware/camera2/params/DynamicRangeProfiles.java
@@ -277,7 +277,7 @@
      * profile.</p>
      *
      * @return non-modifiable set of dynamic range profiles
-     * @throws IllegalArgumentException - If the profile argument is not
+     * @throws IllegalArgumentException   If the profile argument is not
      *                                    within the list returned by
      *                                    getSupportedProfiles()
      *
@@ -303,7 +303,7 @@
      *
      * @return true if the given profile is not suitable for latency sensitive use cases, false
      *         otherwise
-     * @throws IllegalArgumentException - If the profile argument is not
+     * @throws IllegalArgumentException   If the profile argument is not
      *                                    within the list returned by
      *                                    getSupportedProfiles()
      *
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
index 4bf9a74..f701ec3 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
@@ -39,6 +39,7 @@
      * See {@link FingerprintSensorProperties.SensorType}.
      */
     public final @FingerprintSensorProperties.SensorType int sensorType;
+    public final boolean halControlsIllumination;
 
     private final List<SensorLocationInternal> mSensorLocations;
 
@@ -46,6 +47,7 @@
             @SensorProperties.Strength int strength, int maxEnrollmentsPerUser,
             @NonNull List<ComponentInfoInternal> componentInfo,
             @FingerprintSensorProperties.SensorType int sensorType,
+            boolean halControlsIllumination,
             boolean resetLockoutRequiresHardwareAuthToken,
             @NonNull List<SensorLocationInternal> sensorLocations) {
         // IBiometricsFingerprint@2.1 handles lockout in the framework, so the challenge is not
@@ -55,6 +57,7 @@
         super(sensorId, strength, maxEnrollmentsPerUser, componentInfo,
             resetLockoutRequiresHardwareAuthToken, false /* resetLockoutRequiresChallenge */);
         this.sensorType = sensorType;
+        this.halControlsIllumination = halControlsIllumination;
         this.mSensorLocations = List.copyOf(sensorLocations);
     }
 
@@ -68,14 +71,15 @@
             boolean resetLockoutRequiresHardwareAuthToken) {
         // TODO(b/179175438): Value should be provided from the HAL
         this(sensorId, strength, maxEnrollmentsPerUser, componentInfo, sensorType,
-                resetLockoutRequiresHardwareAuthToken, List.of(new SensorLocationInternal(
-                        "" /* displayId */,  540 /* sensorLocationX */, 1636 /* sensorLocationY */,
-                        130 /* sensorRadius */)));
+                false /* halControlsIllumination */, resetLockoutRequiresHardwareAuthToken,
+                List.of(new SensorLocationInternal("" /* displayId */, 540 /* sensorLocationX */,
+                        1636 /* sensorLocationY */, 130 /* sensorRadius */)));
     }
 
     protected FingerprintSensorPropertiesInternal(Parcel in) {
         super(in);
         sensorType = in.readInt();
+        halControlsIllumination = in.readBoolean();
         mSensorLocations = in.createTypedArrayList(SensorLocationInternal.CREATOR);
     }
 
@@ -101,6 +105,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
         dest.writeInt(sensorType);
+        dest.writeBoolean(halControlsIllumination);
         dest.writeTypedList(mSensorLocations);
     }
 
diff --git a/core/java/android/hardware/input/InputManagerInternal.java b/core/java/android/hardware/input/InputManagerInternal.java
index b37c27c..fc6bc55 100644
--- a/core/java/android/hardware/input/InputManagerInternal.java
+++ b/core/java/android/hardware/input/InputManagerInternal.java
@@ -75,8 +75,15 @@
     /**
      * Sets the display id that the MouseCursorController will be forced to target. Pass
      * {@link android.view.Display#INVALID_DISPLAY} to clear the override.
+     *
+     * Note: This method generally blocks until the pointer display override has propagated.
+     * When setting a new override, the caller should ensure that an input device that can control
+     * the mouse pointer is connected. If a new override is set when no such input device is
+     * connected, the caller may be blocked for an arbitrary period of time.
+     *
+     * @return true if the pointer displayId was set successfully, or false if it fails.
      */
-    public abstract void setVirtualMousePointerDisplayId(int pointerDisplayId);
+    public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId);
 
     /**
      * Gets the display id that the MouseCursorController is being forced to target. Returns
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index ecdc803..022d213 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -1361,6 +1361,18 @@
             return false;
         }
 
+        // Apps with PROPERTY_NO_APP_DATA_STORAGE should not be allowed in scoped storage
+        final String packageName = AppGlobals.getInitialPackage();
+        try {
+            final PackageManager.Property noAppStorageProp = packageManager.getProperty(
+                    PackageManager.PROPERTY_NO_APP_DATA_STORAGE, packageName);
+            if (noAppStorageProp != null && noAppStorageProp.getBoolean()) {
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException ignore) {
+            // Property not defined for the package
+        }
+
         boolean defaultScopedStorage = Compatibility.isChangeEnabled(DEFAULT_SCOPED_STORAGE);
         boolean forceEnableScopedStorage = Compatibility.isChangeEnabled(
                 FORCE_ENABLE_SCOPED_STORAGE);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index a64e63e..196f2f9 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2179,7 +2179,10 @@
             }
         } else {
             UserInfo userInfo = getUserInfo(mUserId);
-            return userInfo == null ? "" : userInfo.name;
+            if (userInfo != null && userInfo.name != null) {
+                return userInfo.name;
+            }
+            return "";
         }
     }
 
diff --git a/core/java/android/os/logcat/ILogcatManagerService.aidl b/core/java/android/os/logcat/ILogcatManagerService.aidl
index 29b4570..67a930a 100644
--- a/core/java/android/os/logcat/ILogcatManagerService.aidl
+++ b/core/java/android/os/logcat/ILogcatManagerService.aidl
@@ -42,31 +42,4 @@
      * @param fd  The FD (Socket) of client who makes the request.
      */
     void finishThread(in int uid, in int gid, in int pid, in int fd);
-
-
-    /**
-     * The function is called by UX component to notify
-     * LogcatManagerService that the user approved
-     * the privileged log data access.
-     *
-     * @param uid The UID of client who makes the request.
-     * @param gid The GID of client who makes the request.
-     * @param pid The PID of client who makes the request.
-     * @param fd  The FD (Socket) of client who makes the request.
-     */
-    void approve(in int uid, in int gid, in int pid, in int fd);
-
-
-    /**
-     * The function is called by UX component to notify
-     * LogcatManagerService that the user declined
-     * the privileged log data access.
-     *
-     * @param uid The UID of client who makes the request.
-     * @param gid The GID of client who makes the request.
-     * @param pid The PID of client who makes the request.
-     * @param fd  The FD (Socket) of client who makes the request.
-     */
-    void decline(in int uid, in int gid, in int pid, in int fd);
 }
-
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 0e5a65c..77c0067 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -1292,7 +1292,8 @@
                 USER_MISSED_LOW_RING_VOLUME,
                 USER_MISSED_NO_VIBRATE,
                 USER_MISSED_CALL_SCREENING_SERVICE_SILENCED,
-                USER_MISSED_CALL_FILTERS_TIMEOUT
+                USER_MISSED_CALL_FILTERS_TIMEOUT,
+                USER_MISSED_NEVER_RANG
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface MissedReason {}
@@ -1383,6 +1384,13 @@
         public static final long USER_MISSED_CALL_FILTERS_TIMEOUT = 1 << 22;
 
         /**
+         * When {@link CallLog.Calls#TYPE} is {@link CallLog.Calls#MISSED_TYPE}, set this bit when
+         * the call ended before ringing.
+         * @hide
+         */
+        public static final long USER_MISSED_NEVER_RANG = 1 << 23;
+
+        /**
          * Where the {@link CallLog.Calls#TYPE} is {@link CallLog.Calls#MISSED_TYPE},
          * indicates factors which may have lead the user to miss the call.
          * <P>Type: INTEGER</P>
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1ef1ac5..dac54cf 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7232,13 +7232,6 @@
          */
         public static final String LOCATION_SHOW_SYSTEM_OPS = "locationShowSystemOps";
 
-
-        /**
-         * Whether or not an indicator experiment has started.
-         * @hide
-         */
-        public static final String LOCATION_INDICATOR_EXPERIMENT_STARTED =
-                "locationIndicatorExperimentStarted";
         /**
          * A flag containing settings used for biometric weak
          * @hide
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 425dbb9..0ec95c6 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -904,7 +904,6 @@
             // based on its default wallpaper color hints.
             mShouldDim = dimAmount != 0f || mShouldDimByDefault;
             updateSurfaceDimming();
-            updateSurface(false, false, true);
         }
 
         private void updateSurfaceDimming() {
@@ -941,6 +940,7 @@
             } else {
                 Log.v(TAG, "Setting wallpaper dimming: " + 0);
                 surfaceControlTransaction.setAlpha(mBbqSurfaceControl, 1.0f).apply();
+                updateSurface(false, false, true);
             }
 
             mPreviousWallpaperDimAmount = mWallpaperDimAmount;
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 3be4c3ed..24ded93 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -50,12 +50,21 @@
     public static final String SETTINGS_ENABLE_SECURITY_HUB = "settings_enable_security_hub";
     /** @hide */
     public static final String SETTINGS_SUPPORT_LARGE_SCREEN = "settings_support_large_screen";
+
     /**
      * Support per app's language selection
      * @hide
      */
     public static final String SETTINGS_APP_LANGUAGE_SELECTION = "settings_app_language_selection";
 
+    /**
+     * Support locale opt-out and opt-in switch for per app's language.
+     * @hide
+     */
+    public static final String SETTINGS_APP_LOCALE_OPT_IN_ENABLED =
+            "settings_app_locale_opt_in_enabled";
+
+
     /** @hide */
     public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS =
             "settings_enable_monitor_phantom_procs";
@@ -97,6 +106,7 @@
         DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true");
         DEFAULT_FLAGS.put("settings_search_always_expand", "true");
         DEFAULT_FLAGS.put(SETTINGS_APP_LANGUAGE_SELECTION, "true");
+        DEFAULT_FLAGS.put(SETTINGS_APP_LOCALE_OPT_IN_ENABLED, "true");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true");
         DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
         DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");
@@ -106,6 +116,7 @@
     static {
         PERSISTENT_FLAGS = new HashSet<>();
         PERSISTENT_FLAGS.add(SETTINGS_APP_LANGUAGE_SELECTION);
+        PERSISTENT_FLAGS.add(SETTINGS_APP_LOCALE_OPT_IN_ENABLED);
         PERSISTENT_FLAGS.add(SETTINGS_SUPPORT_LARGE_SCREEN);
         PERSISTENT_FLAGS.add(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS);
         PERSISTENT_FLAGS.add(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME);
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index fd557e7..2e48c2b 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -719,9 +719,14 @@
 
     private void releaseSurfaces(boolean releaseSurfacePackage) {
         mSurfaceAlpha = 1f;
-
-        synchronized (mSurfaceControlLock) {
+	
+        mSurfaceLock.lock();
+        try {
             mSurface.destroy();
+        } finally {
+            mSurfaceLock.unlock();
+        }
+        synchronized (mSurfaceControlLock) {
             if (mBlastBufferQueue != null) {
                 mBlastBufferQueue.destroy();
                 mBlastBufferQueue = null;
@@ -770,105 +775,99 @@
             Transaction surfaceUpdateTransaction) {
         boolean realSizeChanged = false;
 
-        mSurfaceLock.lock();
-        try {
-            mDrawingStopped = !mVisible;
+        mDrawingStopped = !mVisible;
 
-            if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
-                    + "Cur surface: " + mSurface);
+        if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+                + "Cur surface: " + mSurface);
 
-            // If we are creating the surface control or the parent surface has not
-            // changed, then set relative z. Otherwise allow the parent
-            // SurfaceChangedCallback to update the relative z. This is needed so that
-            // we do not change the relative z before the server is ready to swap the
-            // parent surface.
-            if (creating) {
-                updateRelativeZ(surfaceUpdateTransaction);
-                if (mSurfacePackage != null) {
-                    reparentSurfacePackage(surfaceUpdateTransaction, mSurfacePackage);
-                }
+        // If we are creating the surface control or the parent surface has not
+        // changed, then set relative z. Otherwise allow the parent
+        // SurfaceChangedCallback to update the relative z. This is needed so that
+        // we do not change the relative z before the server is ready to swap the
+        // parent surface.
+        if (creating) {
+            updateRelativeZ(surfaceUpdateTransaction);
+            if (mSurfacePackage != null) {
+                reparentSurfacePackage(surfaceUpdateTransaction, mSurfacePackage);
             }
-            mParentSurfaceSequenceId = viewRoot.getSurfaceSequenceId();
-
-            if (mViewVisibility) {
-                surfaceUpdateTransaction.show(mSurfaceControl);
-            } else {
-                surfaceUpdateTransaction.hide(mSurfaceControl);
-            }
-
-
-
-            updateBackgroundVisibility(surfaceUpdateTransaction);
-            updateBackgroundColor(surfaceUpdateTransaction);
-            if (mUseAlpha) {
-                float alpha = getFixedAlpha();
-                surfaceUpdateTransaction.setAlpha(mSurfaceControl, alpha);
-                mSurfaceAlpha = alpha;
-            }
-
-            surfaceUpdateTransaction.setCornerRadius(mSurfaceControl, mCornerRadius);
-            if ((sizeChanged || hintChanged) && !creating) {
-                setBufferSize(surfaceUpdateTransaction);
-            }
-            if (sizeChanged || creating || !isHardwareAccelerated()) {
-
-                // Set a window crop when creating the surface or changing its size to
-                // crop the buffer to the surface size since the buffer producer may
-                // use SCALING_MODE_SCALE and submit a larger size than the surface
-                // size.
-                if (mClipSurfaceToBounds && mClipBounds != null) {
-                    surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mClipBounds);
-                } else {
-                    surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mSurfaceWidth,
-                            mSurfaceHeight);
-                }
-
-                surfaceUpdateTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth,
-                            mSurfaceHeight);
-
-                if (isHardwareAccelerated()) {
-                    // This will consume the passed in transaction and the transaction will be
-                    // applied on a render worker thread.
-                    replacePositionUpdateListener(mSurfaceWidth, mSurfaceHeight);
-                } else {
-                    onSetSurfacePositionAndScale(surfaceUpdateTransaction, mSurfaceControl,
-                            mScreenRect.left /*positionLeft*/,
-                            mScreenRect.top /*positionTop*/,
-                            mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/,
-                            mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/);
-                }
-                if (DEBUG_POSITION) {
-                    Log.d(TAG, String.format(
-                            "%d performSurfaceTransaction %s "
-                                + "position = [%d, %d, %d, %d] surfaceSize = %dx%d",
-                            System.identityHashCode(this),
-                            isHardwareAccelerated() ? "RenderWorker" : "UI Thread",
-                            mScreenRect.left, mScreenRect.top, mScreenRect.right,
-                            mScreenRect.bottom, mSurfaceWidth, mSurfaceHeight));
-                }
-            }
-            applyTransactionOnVriDraw(surfaceUpdateTransaction);
-            updateEmbeddedAccessibilityMatrix(false);
-
-            mSurfaceFrame.left = 0;
-            mSurfaceFrame.top = 0;
-            if (translator == null) {
-                mSurfaceFrame.right = mSurfaceWidth;
-                mSurfaceFrame.bottom = mSurfaceHeight;
-            } else {
-                float appInvertedScale = translator.applicationInvertedScale;
-                mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f);
-                mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f);
-            }
-            final int surfaceWidth = mSurfaceFrame.right;
-            final int surfaceHeight = mSurfaceFrame.bottom;
-            realSizeChanged = mLastSurfaceWidth != surfaceWidth
-                    || mLastSurfaceHeight != surfaceHeight;
-            mLastSurfaceWidth = surfaceWidth;
-            mLastSurfaceHeight = surfaceHeight;
-        } finally {
-            mSurfaceLock.unlock();
         }
+        mParentSurfaceSequenceId = viewRoot.getSurfaceSequenceId();
+
+        if (mViewVisibility) {
+            surfaceUpdateTransaction.show(mSurfaceControl);
+        } else {
+            surfaceUpdateTransaction.hide(mSurfaceControl);
+        }
+
+
+
+        updateBackgroundVisibility(surfaceUpdateTransaction);
+        updateBackgroundColor(surfaceUpdateTransaction);
+        if (mUseAlpha) {
+            float alpha = getFixedAlpha();
+            surfaceUpdateTransaction.setAlpha(mSurfaceControl, alpha);
+            mSurfaceAlpha = alpha;
+        }
+
+        surfaceUpdateTransaction.setCornerRadius(mSurfaceControl, mCornerRadius);
+        if ((sizeChanged || hintChanged) && !creating) {
+            setBufferSize(surfaceUpdateTransaction);
+        }
+        if (sizeChanged || creating || !isHardwareAccelerated()) {
+            // Set a window crop when creating the surface or changing its size to
+            // crop the buffer to the surface size since the buffer producer may
+            // use SCALING_MODE_SCALE and submit a larger size than the surface
+            // size.
+            if (mClipSurfaceToBounds && mClipBounds != null) {
+                surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mClipBounds);
+            } else {
+                surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mSurfaceWidth,
+                        mSurfaceHeight);
+            }
+
+            surfaceUpdateTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth,
+                        mSurfaceHeight);
+
+            if (isHardwareAccelerated()) {
+                // This will consume the passed in transaction and the transaction will be
+                // applied on a render worker thread.
+                replacePositionUpdateListener(mSurfaceWidth, mSurfaceHeight);
+            } else {
+                onSetSurfacePositionAndScale(surfaceUpdateTransaction, mSurfaceControl,
+                        mScreenRect.left /*positionLeft*/,
+                        mScreenRect.top /*positionTop*/,
+                        mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/,
+                        mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/);
+            }
+            if (DEBUG_POSITION) {
+                Log.d(TAG, String.format(
+                        "%d performSurfaceTransaction %s "
+                            + "position = [%d, %d, %d, %d] surfaceSize = %dx%d",
+                        System.identityHashCode(this),
+                        isHardwareAccelerated() ? "RenderWorker" : "UI Thread",
+                        mScreenRect.left, mScreenRect.top, mScreenRect.right,
+                        mScreenRect.bottom, mSurfaceWidth, mSurfaceHeight));
+            }
+        }
+        applyTransactionOnVriDraw(surfaceUpdateTransaction);
+        updateEmbeddedAccessibilityMatrix(false);
+         mSurfaceFrame.left = 0;
+        mSurfaceFrame.top = 0;
+        if (translator == null) {
+            mSurfaceFrame.right = mSurfaceWidth;
+            mSurfaceFrame.bottom = mSurfaceHeight;
+        } else {
+            float appInvertedScale = translator.applicationInvertedScale;
+            mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f);
+            mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f);
+        }
+        final int surfaceWidth = mSurfaceFrame.right;
+        final int surfaceHeight = mSurfaceFrame.bottom;
+        realSizeChanged = mLastSurfaceWidth != surfaceWidth
+                || mLastSurfaceHeight != surfaceHeight;
+        mLastSurfaceWidth = surfaceWidth;
+        mLastSurfaceHeight = surfaceHeight;
+
         return realSizeChanged;
     }
 
@@ -1103,21 +1102,30 @@
      *                          Surface for compatibility reasons.
      */
     private void copySurface(boolean surfaceControlCreated, boolean bufferSizeChanged) {
-        if (surfaceControlCreated) {
-            mSurface.copyFrom(mBlastBufferQueue);
-        }
+        // Some legacy applications use the underlying native {@link Surface} object
+        // as a key to whether anything has changed. In these cases, updates to the
+        // existing {@link Surface} will be ignored when the size changes.
+        // Therefore, we must explicitly recreate the {@link Surface} in these
+        // cases.
+        boolean needsWorkaround = bufferSizeChanged &&
+            getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O;
+       if (!surfaceControlCreated && !needsWorkaround) {
+           return;
+       }
+       mSurfaceLock.lock();
+       try {
+           if (surfaceControlCreated) {
+               mSurface.copyFrom(mBlastBufferQueue);
+           }
 
-        if (bufferSizeChanged && getContext().getApplicationInfo().targetSdkVersion
-                < Build.VERSION_CODES.O) {
-            // Some legacy applications use the underlying native {@link Surface} object
-            // as a key to whether anything has changed. In these cases, updates to the
-            // existing {@link Surface} will be ignored when the size changes.
-            // Therefore, we must explicitly recreate the {@link Surface} in these
-            // cases.
-            if (mBlastBufferQueue != null) {
-                mSurface.transferFrom(mBlastBufferQueue.createSurfaceWithHandle());
-            }
-        }
+           if (needsWorkaround) {
+               if (mBlastBufferQueue != null) {
+                   mSurface.transferFrom(mBlastBufferQueue.createSurfaceWithHandle());
+               }
+           }
+       } finally {
+           mSurfaceLock.unlock();
+       }
     }
 
     private void setBufferSize(Transaction transaction) {
@@ -1200,8 +1208,10 @@
         }
         mTransformHint = viewRoot.getBufferTransformHint();
         mBlastSurfaceControl.setTransformHint(mTransformHint);
+
         mBlastBufferQueue = new BLASTBufferQueue(name, false /* updateDestinationFrame */);
         mBlastBufferQueue.update(mBlastSurfaceControl, mSurfaceWidth, mSurfaceHeight, mFormat);
+        mBlastBufferQueue.setTransactionHangCallback(ViewRootImpl.sTransactionHangCallback);
     }
 
     private void onDrawFinished() {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 6fc16c6..2475e2c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -8206,24 +8206,25 @@
         if (canNotifyAutofillEnterExitEvent()) {
             AutofillManager afm = getAutofillManager();
             if (afm != null) {
-                if (enter && isFocused()) {
+                if (enter) {
                     // We have not been laid out yet, hence cannot evaluate
                     // whether this view is visible to the user, we will do
                     // the evaluation once layout is complete.
                     if (!isLaidOut()) {
                         mPrivateFlags3 |= PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
                     } else if (isVisibleToUser()) {
-                        // TODO This is a potential problem that View gets focus before it's visible
-                        // to User. Ideally View should handle the event when isVisibleToUser()
-                        // becomes true where it should issue notifyViewEntered().
-                        afm.notifyViewEntered(this);
-                    } else {
-                        afm.enableFillRequestActivityStarted(this);
+                        if (isFocused()) {
+                            // TODO This is a potential problem that View gets focus before it's
+                            // visible to User. Ideally View should handle the event when
+                            // isVisibleToUser() becomes true where it should issue
+                            // notifyViewEntered().
+                            afm.notifyViewEntered(this);
+                        } else {
+                            afm.notifyViewEnteredForFillDialog(this);
+                        }
                     }
-                } else if (!enter && !isFocused()) {
+                } else if (!isFocused()) {
                     afm.notifyViewExited(this);
-                } else if (enter) {
-                    afm.enableFillRequestActivityStarted(this);
                 }
             }
         }
@@ -11752,6 +11753,7 @@
                 && (info.mHandwritingArea == null || !isAutoHandwritingEnabled())) {
             if (info.mPositionUpdateListener != null) {
                 mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
+                info.mPositionUpdateListener = null;
                 info.mPositionChangedUpdate = null;
             }
         } else {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 335cd27..0bdbfbc 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -590,7 +590,6 @@
     @Nullable
     int mContentCaptureEnabled = CONTENT_CAPTURE_ENABLED_NOT_CHECKED;
     boolean mPerformContentCapture;
-    boolean mPerformAutoFill;
 
 
     boolean mReportNextDraw;
@@ -858,6 +857,28 @@
      */
     private Bundle mRelayoutBundle = new Bundle();
 
+    private static volatile boolean sAnrReported = false;
+    static BLASTBufferQueue.TransactionHangCallback sTransactionHangCallback =
+        new BLASTBufferQueue.TransactionHangCallback() {
+            @Override
+            public void onTransactionHang(boolean isGPUHang) {
+                if (isGPUHang && !sAnrReported) {
+                    sAnrReported = true;
+                    try {
+                        ActivityManager.getService().appNotResponding(
+                            "Buffer processing hung up due to stuck fence. Indicates GPU hang");
+                    } catch (RemoteException e) {
+                        // We asked the system to crash us, but the system
+                        // already crashed. Unfortunately things may be
+                        // out of control.
+                    }
+                } else {
+                    // TODO: Do something with this later. For now we just ANR
+                    // in dequeue buffer later like we always have.
+                }
+            }
+        };
+
     private String mTag = TAG;
 
     public ViewRootImpl(Context context, Display display) {
@@ -890,7 +911,6 @@
         mPreviousTransparentRegion = new Region();
         mFirst = true; // true for the first time the view is added
         mPerformContentCapture = true; // also true for the first time the view is added
-        mPerformAutoFill = true;
         mAdded = false;
         mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                 context);
@@ -2100,6 +2120,7 @@
         }
         mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
                 mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);
+        mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);
         Surface blastSurface = mBlastBufferQueue.createSurface();
         // Only call transferFrom if the surface has changed to prevent inc the generation ID and
         // causing EGL resources to be recreated.
@@ -4196,26 +4217,6 @@
         });
     }
 
-    @Nullable
-    private void registerFrameDrawingCallbackForBlur() {
-        if (!isHardwareEnabled()) {
-            return;
-        }
-        final boolean hasBlurUpdates = mBlurRegionAggregator.hasUpdates();
-        final boolean needsCallbackForBlur = hasBlurUpdates || mBlurRegionAggregator.hasRegions();
-
-        if (!needsCallbackForBlur) {
-            return;
-        }
-
-        final BackgroundBlurDrawable.BlurRegion[] blurRegionsForFrame =
-                mBlurRegionAggregator.getBlurRegionsCopyForRT();
-
-        // The callback will run on the render thread.
-        registerRtFrameCallback((frame) -> mBlurRegionAggregator
-                .dispatchBlurTransactionIfNeeded(frame, blurRegionsForFrame, hasBlurUpdates));
-    }
-
     private void registerCallbackForPendingTransactions() {
         registerRtFrameCallback(new FrameDrawingCallback() {
             @Override
@@ -4253,7 +4254,6 @@
         mIsDrawing = true;
         Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
 
-        registerFrameDrawingCallbackForBlur();
         addFrameCommitCallbackIfNeeded();
 
         boolean usingAsyncReport = isHardwareEnabled() && mSyncBufferCallback != null;
@@ -4329,18 +4329,6 @@
         if (mPerformContentCapture) {
             performContentCaptureInitialReport();
         }
-
-        if (mPerformAutoFill) {
-            notifyEnterForAutoFillIfNeeded();
-        }
-    }
-
-    private void notifyEnterForAutoFillIfNeeded() {
-        mPerformAutoFill = false;
-        final AutofillManager afm = getAutofillManager();
-        if (afm != null) {
-            afm.notifyViewEnteredForActivityStarted(mView);
-        }
     }
 
     /**
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 0a75992..dcedb30 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -102,6 +102,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import sun.misc.Cleaner;
 
@@ -645,16 +646,6 @@
     private boolean mEnabledForAugmentedAutofillOnly;
 
     /**
-     * Indicates whether there are any fields that need to do a fill request
-     * after the activity starts.
-     *
-     * Note: This field will be set to true multiple times if there are many
-     * autofillable views. So needs to check mIsFillRequested at the same time to
-     * avoid re-trigger autofill.
-     */
-    private boolean mRequireAutofill;
-
-    /**
      * Indicates whether there is already a field to do a fill request after
      * the activity started.
      *
@@ -663,7 +654,7 @@
      * triggered autofill, it is unnecessary to trigger again through
      * AutofillManager#notifyViewEnteredForActivityStarted.
      */
-    private boolean mIsFillRequested;
+    private AtomicBoolean mIsFillRequested;
 
     @Nullable private List<AutofillId> mFillDialogTriggerIds;
 
@@ -811,8 +802,7 @@
         mContext = Objects.requireNonNull(context, "context cannot be null");
         mService = service;
         mOptions = context.getAutofillOptions();
-        mIsFillRequested = false;
-        mRequireAutofill = false;
+        mIsFillRequested = new AtomicBoolean(false);
 
         mIsFillDialogEnabled = DeviceConfig.getBoolean(
                 DeviceConfig.NAMESPACE_AUTOFILL,
@@ -1113,22 +1103,31 @@
     }
 
     /**
-     * The view have the allowed autofill hints, marked to perform a fill request after layout if
-     * the field does not trigger a fill request.
+     * The {@link #DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED} is {@code true} or the view have
+     * the allowed autofill hints, performs a fill request to know there is any field supported
+     * fill dialog.
      *
      * @hide
      */
-    public void enableFillRequestActivityStarted(View v) {
-        if (mRequireAutofill) {
+    public void notifyViewEnteredForFillDialog(View v) {
+        // Skip if the fill request has been performed for a view.
+        if (mIsFillRequested.get()) {
             return;
         }
 
         if (mIsFillDialogEnabled
                 || ArrayUtils.containsAny(v.getAutofillHints(), mFillDialogEnabledHints)) {
             if (sDebug) {
-                Log.d(TAG, "Trigger fill request at starting");
+                Log.d(TAG, "Trigger fill request at view entered");
             }
-            mRequireAutofill = true;
+
+            // Note: No need for atomic getAndSet as this method is called on the UI thread.
+            mIsFillRequested.set(true);
+
+            int flags = FLAG_SUPPORTS_FILL_DIALOG;
+            flags |= FLAG_VIEW_NOT_FOCUSED;
+            // use root view, so autofill UI does not trigger immediately.
+            notifyViewEntered(v.getRootView(), flags);
         }
     }
 
@@ -1136,25 +1135,6 @@
         return mIsFillDialogEnabled || !ArrayUtils.isEmpty(mFillDialogEnabledHints);
     }
 
-    /**
-     * Notify autofill to do a fill request while the activity started.
-     *
-     * @hide
-     */
-    public void notifyViewEnteredForActivityStarted(@NonNull View view) {
-        if (!hasAutofillFeature() || !hasFillDialogUiFeature()) {
-            return;
-        }
-
-        if (!mRequireAutofill || mIsFillRequested) {
-            return;
-        }
-
-        int flags = FLAG_SUPPORTS_FILL_DIALOG;
-        flags |= FLAG_VIEW_NOT_FOCUSED;
-        notifyViewEntered(view, flags);
-    }
-
     private int getImeStateFlag(View v) {
         final WindowInsets rootWindowInsets = v.getRootWindowInsets();
         if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsets.Type.ime())) {
@@ -1203,7 +1183,7 @@
         }
         AutofillCallback callback;
         synchronized (mLock) {
-            mIsFillRequested = true;
+            mIsFillRequested.set(true);
             callback = notifyViewEnteredLocked(view, flags);
         }
 
@@ -2119,8 +2099,7 @@
         mFillableIds = null;
         mSaveTriggerId = null;
         mIdShownFillUi = null;
-        mIsFillRequested = false;
-        mRequireAutofill = false;
+        mIsFillRequested.set(false);
         mShowAutofillDialogCalled = false;
         mFillDialogTriggerIds = null;
         if (resetEnteredIds) {
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index f0a685e..3fee914 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -245,7 +245,7 @@
                     SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP,
                     DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP);
 
-    private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 250;
+    private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 125;
 
     @VisibleForTesting
     int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
diff --git a/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java b/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java
index 402d7fe..4e1ecc2 100644
--- a/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java
+++ b/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -232,9 +233,12 @@
         private final ArraySet<BackgroundBlurDrawable> mDrawables = new ArraySet();
         @GuardedBy("mRtLock")
         private final LongSparseArray<ArraySet<Runnable>> mFrameRtUpdates = new LongSparseArray();
+        private long mLastFrameNumber = 0;
+        private BlurRegion[] mLastFrameBlurRegions = null;
         private final ViewRootImpl mViewRoot;
         private BlurRegion[] mTmpBlurRegionsForFrame = new BlurRegion[0];
         private boolean mHasUiUpdates;
+        private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener;
 
         public Aggregator(ViewRootImpl viewRoot) {
             mViewRoot = viewRoot;
@@ -277,6 +281,38 @@
                     Log.d(TAG, "Remove " + drawable);
                 }
             }
+
+            if (mOnPreDrawListener == null && mViewRoot.getView() != null
+                    && hasRegions()) {
+                registerPreDrawListener();
+            }
+        }
+
+        private void registerPreDrawListener() {
+            mOnPreDrawListener = () -> {
+                final boolean hasUiUpdates = hasUpdates();
+
+                if (hasUiUpdates || hasRegions()) {
+                    final BlurRegion[] blurRegionsForNextFrame = getBlurRegionsCopyForRT();
+
+                    mViewRoot.registerRtFrameCallback(frame -> {
+                        synchronized (mRtLock) {
+                            mLastFrameNumber = frame;
+                            mLastFrameBlurRegions = blurRegionsForNextFrame;
+                            handleDispatchBlurTransactionLocked(
+                                    frame, blurRegionsForNextFrame, hasUiUpdates);
+                        }
+                    });
+                }
+                if (!hasRegions() && mViewRoot.getView() != null) {
+                    mViewRoot.getView().getViewTreeObserver()
+                            .removeOnPreDrawListener(mOnPreDrawListener);
+                    mOnPreDrawListener = null;
+                }
+                return true;
+            };
+
+            mViewRoot.getView().getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
         }
 
         // Called from a thread pool
@@ -290,7 +326,14 @@
                     mFrameRtUpdates.put(frameNumber, frameRtUpdates);
                 }
                 frameRtUpdates.add(update);
+
+                if (mLastFrameNumber == frameNumber) {
+                    // The transaction for this frame has already been sent, so we have to manually
+                    // trigger sending a transaction here in order to apply this position update
+                    handleDispatchBlurTransactionLocked(frameNumber, mLastFrameBlurRegions, true);
+                }
             }
+
         }
 
         /**
@@ -329,29 +372,27 @@
         /**
          * Called on RenderThread.
          *
-         * @return all blur regions if there are any ui or position updates for this frame,
-         *         null otherwise
+         * @return true if it is necessary to send an update to Sf this frame
          */
+        @GuardedBy("mRtLock")
         @VisibleForTesting
-        public float[][] getBlurRegionsToDispatchToSf(long frameNumber,
-                BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) {
-            synchronized (mRtLock) {
-                if (!hasUiUpdatesForFrame && (mFrameRtUpdates.size() == 0
-                            || mFrameRtUpdates.keyAt(0) > frameNumber)) {
-                    return null;
-                }
+        public float[][] getBlurRegionsForFrameLocked(long frameNumber,
+                BlurRegion[] blurRegionsForFrame, boolean forceUpdate) {
+            if (!forceUpdate && (mFrameRtUpdates.size() == 0
+                        || mFrameRtUpdates.keyAt(0) > frameNumber)) {
+                return null;
+            }
 
-                // mFrameRtUpdates holds position updates coming from a thread pool span from
-                // RenderThread. At this point, all position updates for frame frameNumber should
-                // have been added to mFrameRtUpdates.
-                // Here, we apply all updates for frames <= frameNumber in case some previous update
-                // has been missed. This also protects mFrameRtUpdates from memory leaks.
-                while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) {
-                    final ArraySet<Runnable> frameUpdates = mFrameRtUpdates.valueAt(0);
-                    mFrameRtUpdates.removeAt(0);
-                    for (int i = 0; i < frameUpdates.size(); i++) {
-                        frameUpdates.valueAt(i).run();
-                    }
+            // mFrameRtUpdates holds position updates coming from a thread pool span from
+            // RenderThread. At this point, all position updates for frame frameNumber should
+            // have been added to mFrameRtUpdates.
+            // Here, we apply all updates for frames <= frameNumber in case some previous update
+            // has been missed. This also protects mFrameRtUpdates from memory leaks.
+            while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) {
+                final ArraySet<Runnable> frameUpdates = mFrameRtUpdates.valueAt(0);
+                mFrameRtUpdates.removeAt(0);
+                for (int i = 0; i < frameUpdates.size(); i++) {
+                    frameUpdates.valueAt(i).run();
                 }
             }
 
@@ -370,13 +411,13 @@
         }
 
         /**
-         * Called on RenderThread in FrameDrawingCallback.
-         * Dispatch all blur regions if there are any ui or position updates.
+         * Dispatch all blur regions if there are any ui or position updates for that frame.
          */
-        public void dispatchBlurTransactionIfNeeded(long frameNumber,
-                BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) {
-            final float[][] blurRegionsArray = getBlurRegionsToDispatchToSf(frameNumber,
-                    blurRegionsForFrame, hasUiUpdatesForFrame);
+        @GuardedBy("mRtLock")
+        private void handleDispatchBlurTransactionLocked(long frameNumber, BlurRegion[] blurRegions,
+                boolean forceUpdate) {
+            float[][] blurRegionsArray =
+                    getBlurRegionsForFrameLocked(frameNumber, blurRegions, forceUpdate);
             if (blurRegionsArray != null) {
                 mViewRoot.dispatchBlurRegions(blurRegionsArray, frameNumber);
             }
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 46b4630..ef8f2db 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -88,7 +88,7 @@
             in int notificationLocation, boolean modifiedBeforeSending);
     void onNotificationSettingsViewed(String key);
     void onNotificationBubbleChanged(String key, boolean isBubble, int flags);
-    void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, boolean isBubbleSuppressed);
+    void onBubbleMetadataFlagChanged(String key, int flags);
     void hideCurrentInputMethodForBubbles();
     void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName);
     oneway void clearInlineReplyUriPermissions(String key);
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index c0f7b41..4af28ea 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -47,6 +47,43 @@
     return env;
 }
 
+  struct {
+    jmethodID onTransactionHang;
+} gTransactionHangCallback;
+
+class TransactionHangCallbackWrapper : public LightRefBase<TransactionHangCallbackWrapper> {
+public:
+    explicit TransactionHangCallbackWrapper(JNIEnv* env, jobject jobject) {
+        env->GetJavaVM(&mVm);
+        mTransactionHangObject = env->NewGlobalRef(jobject);
+        LOG_ALWAYS_FATAL_IF(!mTransactionHangObject, "Failed to make global ref");
+    }
+
+    ~TransactionHangCallbackWrapper() {
+        if (mTransactionHangObject) {
+            getenv()->DeleteGlobalRef(mTransactionHangObject);
+            mTransactionHangObject = nullptr;
+        }
+    }
+
+    void onTransactionHang(bool isGpuHang) {
+        if (mTransactionHangObject) {
+            getenv()->CallVoidMethod(mTransactionHangObject,
+                                     gTransactionHangCallback.onTransactionHang, isGpuHang);
+        }
+    }
+
+private:
+    JavaVM* mVm;
+    jobject mTransactionHangObject;
+
+    JNIEnv* getenv() {
+        JNIEnv* env;
+        mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
+        return env;
+    }
+};
+
 static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring jName,
                           jboolean updateDestinationFrame) {
     ScopedUtfChars name(env, jName);
@@ -141,6 +178,20 @@
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
     return queue->isSameSurfaceControl(reinterpret_cast<SurfaceControl*>(surfaceControl));
 }
+  
+static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong ptr,
+                                             jobject transactionHangCallback) {
+    sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
+    if (transactionHangCallback == nullptr) {
+        queue->setTransactionHangCallback(nullptr);
+    } else {
+        sp<TransactionHangCallbackWrapper> wrapper =
+                new TransactionHangCallbackWrapper{env, transactionHangCallback};
+        queue->setTransactionHangCallback([wrapper](bool isGpuHang) {
+            wrapper->onTransactionHang(isGpuHang);
+        });
+    }
+}
 
 static jobject nativeGatherPendingTransactions(JNIEnv* env, jclass clazz, jlong ptr,
                                                jlong frameNum) {
@@ -163,7 +214,10 @@
         {"nativeGetLastAcquiredFrameNum", "(J)J", (void*)nativeGetLastAcquiredFrameNum},
         {"nativeApplyPendingTransactions", "(JJ)V", (void*)nativeApplyPendingTransactions},
         {"nativeIsSameSurfaceControl", "(JJ)Z", (void*)nativeIsSameSurfaceControl},
-        {"nativeGatherPendingTransactions", "(JJ)Landroid/view/SurfaceControl$Transaction;", (void*)nativeGatherPendingTransactions}
+        {"nativeGatherPendingTransactions", "(JJ)Landroid/view/SurfaceControl$Transaction;", (void*)nativeGatherPendingTransactions},
+        {"nativeSetTransactionHangCallback",
+         "(JLandroid/graphics/BLASTBufferQueue$TransactionHangCallback;)V",
+         (void*)nativeSetTransactionHangCallback},
         // clang-format on
 };
 
@@ -180,6 +234,11 @@
     jclass consumer = FindClassOrDie(env, "java/util/function/Consumer");
     gTransactionConsumer.accept =
             GetMethodIDOrDie(env, consumer, "accept", "(Ljava/lang/Object;)V");
+    jclass transactionHangClass =
+            FindClassOrDie(env, "android/graphics/BLASTBufferQueue$TransactionHangCallback");
+    gTransactionHangCallback.onTransactionHang =
+            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", "(Z)V");
+
     return 0;
 }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 217166c..0f328b0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6776,9 +6776,8 @@
         </activity>
 
         <activity android:name="com.android.server.logcat.LogAccessDialogActivity"
-                  android:theme="@style/Theme.DeviceDefault.Dialog.Alert.DayNight"
+                  android:theme="@style/Theme.Translucent.NoTitleBar"
                   android:excludeFromRecents="true"
-                  android:label="@string/log_access_confirmation_title"
                   android:exported="false">
         </activity>
 
@@ -7057,6 +7056,10 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
+        <service android:name="com.android.server.notification.ReviewNotificationPermissionsJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
         <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
             android:exported="false">
             <intent-filter>
diff --git a/core/res/OWNERS b/core/res/OWNERS
index 95d2712..c54638a 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -45,3 +45,4 @@
 
 # Telephony
 per-file res/values/config_telephony.xml = file:/platform/frameworks/opt/telephony:/OWNERS
+per-file res/xml/sms_short_codes.xml = file:/platform/frameworks/opt/telephony:/OWNERS
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index edaf8cf..689ff66 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5451,6 +5451,8 @@
     <bool name="config_supportsHardwareCamToggle">false</bool>
     <!-- Whether a camera intent is launched when the lens cover is toggled -->
     <bool name="config_launchCameraOnCameraLensCoverToggle">true</bool>
+    <!-- Whether changing sensor privacy SW setting requires device to be unlocked -->
+    <bool name="config_sensorPrivacyRequiresAuthentication">true</bool>
 
     <!-- List containing the allowed install sources for accessibility service. -->
     <string-array name="config_accessibility_allowed_install_source" translatable="false"/>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b882123..443f9a6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4661,6 +4661,7 @@
   <java-symbol type="bool" name="config_supportsHardwareMicToggle" />
   <java-symbol type="bool" name="config_supportsHardwareCamToggle" />
   <java-symbol type="bool" name="config_launchCameraOnCameraLensCoverToggle" />
+  <java-symbol type="bool" name="config_sensorPrivacyRequiresAuthentication" />
 
   <java-symbol type="dimen" name="starting_surface_icon_size" />
   <java-symbol type="dimen" name="starting_surface_default_icon_size" />
diff --git a/core/tests/coretests/src/android/view/BlurAggregatorTest.java b/core/tests/coretests/src/android/view/BlurAggregatorTest.java
index b01f275..ded925e 100644
--- a/core/tests/coretests/src/android/view/BlurAggregatorTest.java
+++ b/core/tests/coretests/src/android/view/BlurAggregatorTest.java
@@ -65,7 +65,7 @@
         drawable.setBlurRadius(TEST_BLUR_RADIUS);
         final boolean hasUpdates = mAggregator.hasUpdates();
         final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT();
-        mAggregator.getBlurRegionsToDispatchToSf(TEST_FRAME_NUMBER, blurRegions, hasUpdates);
+        mAggregator.getBlurRegionsForFrameLocked(TEST_FRAME_NUMBER, blurRegions, hasUpdates);
         return drawable;
     }
 
@@ -154,7 +154,7 @@
         assertEquals(1, blurRegions.length);
 
         mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4);
-        mAggregator.getBlurRegionsToDispatchToSf(TEST_FRAME_NUMBER, blurRegions,
+        mAggregator.getBlurRegionsForFrameLocked(TEST_FRAME_NUMBER, blurRegions,
                 mAggregator.hasUpdates());
         assertEquals(1, blurRegions[0].rect.left);
         assertEquals(2, blurRegions[0].rect.top);
@@ -169,7 +169,7 @@
         final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT();
         assertEquals(1, blurRegions.length);
 
-        float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER, blurRegions, hasUpdates);
         assertNull(blurRegionsForSf);
     }
@@ -182,7 +182,7 @@
         final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT();
         assertEquals(1, blurRegions.length);
 
-        float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER, blurRegions, hasUpdates);
         assertNotNull(blurRegionsForSf);
         assertEquals(1, blurRegionsForSf.length);
@@ -197,7 +197,7 @@
         assertEquals(1, blurRegions.length);
 
         mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4);
-        float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER, blurRegions, hasUpdates);
         assertNotNull(blurRegionsForSf);
         assertEquals(1, blurRegionsForSf.length);
@@ -216,7 +216,7 @@
         assertEquals(1, blurRegions.length);
 
         mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4);
-        float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER + 1, blurRegions, hasUpdates);
         assertNotNull(blurRegionsForSf);
         assertEquals(1, blurRegionsForSf.length);
@@ -237,19 +237,19 @@
         assertEquals(2, blurRegions.length);
 
         // Check that an update in one of the drawables triggers a dispatch of all blur regions
-        float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER, blurRegions, hasUpdates);
         assertNotNull(blurRegionsForSf);
         assertEquals(2, blurRegionsForSf.length);
 
         // Check that the Aggregator deleted all position updates for frame TEST_FRAME_NUMBER
-        blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER, blurRegions, /* hasUiUpdates= */ false);
         assertNull(blurRegionsForSf);
 
         // Check that a position update triggers a dispatch of all blur regions
         drawable2.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4);
-        blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER + 1, blurRegions, hasUpdates);
         assertNotNull(blurRegionsForSf);
         assertEquals(2, blurRegionsForSf.length);
@@ -292,7 +292,7 @@
         mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4);
         mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER + 1, 5, 6, 7, 8);
 
-        final float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf(
+        final float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER, blurRegions, /* hasUiUpdates= */ false);
         assertNotNull(blurRegionsForSf);
         assertEquals(1, blurRegionsForSf.length);
@@ -303,7 +303,7 @@
         assertEquals(3f, blurRegionsForSf[0][4]);
         assertEquals(4f, blurRegionsForSf[0][5]);
 
-        final float[][] blurRegionsForSfForNextFrame = mAggregator.getBlurRegionsToDispatchToSf(
+        final float[][] blurRegionsForSfForNextFrame = mAggregator.getBlurRegionsForFrameLocked(
                 TEST_FRAME_NUMBER + 1, blurRegions, /* hasUiUpdates= */ false);
         assertNotNull(blurRegionsForSfForNextFrame);
         assertEquals(1, blurRegionsForSfForNextFrame.length);
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 891c82d..0e8388b 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -2485,6 +2485,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "323235828": {
+      "message": "Delaying app transition for recents animation to finish",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_APP_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/AppTransitionController.java"
+    },
     "327461496": {
       "message": "Complete pause: %s",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java
index 4b723d1..1c41d06 100644
--- a/graphics/java/android/graphics/BLASTBufferQueue.java
+++ b/graphics/java/android/graphics/BLASTBufferQueue.java
@@ -43,6 +43,12 @@
     private static native boolean nativeIsSameSurfaceControl(long ptr, long surfaceControlPtr);
     private static native SurfaceControl.Transaction nativeGatherPendingTransactions(long ptr,
             long frameNumber);
+    private static native void nativeSetTransactionHangCallback(long ptr,
+            TransactionHangCallback callback);
+
+    public interface TransactionHangCallback {
+        void onTransactionHang(boolean isGpuHang);
+    }
 
     /** Create a new connection with the surface flinger. */
     public BLASTBufferQueue(String name, SurfaceControl sc, int width, int height,
@@ -184,4 +190,8 @@
     public SurfaceControl.Transaction gatherPendingTransactions(long frameNumber) {
         return nativeGatherPendingTransactions(mNativeObject, frameNumber);
     }
+
+    public void setTransactionHangCallback(TransactionHangCallback hangCallback) {
+        nativeSetTransactionHangCallback(mNativeObject, hangCallback);
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 82f8a13..faada1a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -777,22 +777,14 @@
         return null;
     }
 
-    private void updateCallbackIfNecessary() {
-        updateCallbackIfNecessary(true /* deferCallbackUntilAllActivitiesCreated */);
-    }
-
     /**
      * Notifies listeners about changes to split states if necessary.
-     *
-     * @param deferCallbackUntilAllActivitiesCreated boolean to indicate whether the split info
-     *                                               callback should be deferred until all the
-     *                                               organized activities have been created.
      */
-    private void updateCallbackIfNecessary(boolean deferCallbackUntilAllActivitiesCreated) {
+    private void updateCallbackIfNecessary() {
         if (mEmbeddingCallback == null) {
             return;
         }
-        if (deferCallbackUntilAllActivitiesCreated && !allActivitiesCreated()) {
+        if (!allActivitiesCreated()) {
             return;
         }
         List<SplitInfo> currentSplitStates = getActiveSplitStates();
@@ -848,9 +840,7 @@
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
             final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
             for (TaskFragmentContainer container : containers) {
-                if (container.getInfo() == null
-                        || container.getInfo().getActivities().size()
-                        != container.collectActivities().size()) {
+                if (!container.taskInfoActivityCountMatchesCreated()) {
                     return false;
                 }
             }
@@ -1035,11 +1025,8 @@
                             && container.getTaskFragmentToken().equals(initialTaskFragmentToken)) {
                         // The onTaskFragmentInfoChanged callback containing this activity has not
                         // reached the client yet, so add the activity to the pending appeared
-                        // activities and send a split info callback to the client before
-                        // {@link Activity#onCreate} is called.
+                        // activities.
                         container.addPendingAppearedActivity(activity);
-                        updateCallbackIfNecessary(
-                                false /* deferCallbackUntilAllActivitiesCreated */);
                         return;
                     }
                 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 35981d3..26bbcbb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -145,6 +145,18 @@
         return allActivities;
     }
 
+    /**
+     * Checks if the count of activities from the same process in task fragment info corresponds to
+     * the ones created and available on the client side.
+     */
+    boolean taskInfoActivityCountMatchesCreated() {
+        if (mInfo == null) {
+            return false;
+        }
+        return mPendingAppearedActivities.isEmpty()
+                && mInfo.getActivities().size() == collectActivities().size();
+    }
+
     ActivityStack toActivityStack() {
         return new ActivityStack(collectActivities(), isEmpty());
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 227494c..31fc6a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -71,7 +71,7 @@
     private long mLastAccessed;
 
     @Nullable
-    private Bubbles.SuppressionChangedListener mSuppressionListener;
+    private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
 
     /** Whether the bubble should show a dot for the notification indicating updated content. */
     private boolean mShowBubbleUpdateDot = true;
@@ -192,13 +192,13 @@
 
     @VisibleForTesting(visibility = PRIVATE)
     public Bubble(@NonNull final BubbleEntry entry,
-            @Nullable final Bubbles.SuppressionChangedListener listener,
+            @Nullable final Bubbles.BubbleMetadataFlagListener listener,
             final Bubbles.PendingIntentCanceledListener intentCancelListener,
             Executor mainExecutor) {
         mKey = entry.getKey();
         mGroupKey = entry.getGroupKey();
         mLocusId = entry.getLocusId();
-        mSuppressionListener = listener;
+        mBubbleMetadataFlagListener = listener;
         mIntentCancelListener = intent -> {
             if (mIntent != null) {
                 mIntent.unregisterCancelListener(mIntentCancelListener);
@@ -606,8 +606,8 @@
             mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
         }
 
-        if (showInShade() != prevShowInShade && mSuppressionListener != null) {
-            mSuppressionListener.onBubbleNotificationSuppressionChange(this);
+        if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) {
+            mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
         }
     }
 
@@ -626,8 +626,8 @@
         } else {
             mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
         }
-        if (prevSuppressed != suppressBubble && mSuppressionListener != null) {
-            mSuppressionListener.onBubbleNotificationSuppressionChange(this);
+        if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) {
+            mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
         }
     }
 
@@ -771,12 +771,17 @@
         return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
     }
 
-    void setShouldAutoExpand(boolean shouldAutoExpand) {
+    @VisibleForTesting
+    public void setShouldAutoExpand(boolean shouldAutoExpand) {
+        boolean prevAutoExpand = shouldAutoExpand();
         if (shouldAutoExpand) {
             enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
         } else {
             disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
         }
+        if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) {
+            mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
+        }
     }
 
     public void setIsBubble(final boolean isBubble) {
@@ -799,6 +804,10 @@
         return (mFlags & option) != 0;
     }
 
+    public int getFlags() {
+        return mFlags;
+    }
+
     @Override
     public String toString() {
         return "Bubble{" + mKey + '}';
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 806c395..f407bdc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -323,7 +323,7 @@
 
     public void initialize() {
         mBubbleData.setListener(mBubbleDataListener);
-        mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged);
+        mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
 
         mBubbleData.setPendingIntentCancelledListener(bubble -> {
             if (bubble.getBubbleIntent() == null) {
@@ -554,11 +554,10 @@
     }
 
     @VisibleForTesting
-    public void onBubbleNotificationSuppressionChanged(Bubble bubble) {
+    public void onBubbleMetadataFlagChanged(Bubble bubble) {
         // Make sure NoMan knows suppression state so that anyone querying it can tell.
         try {
-            mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(),
-                    !bubble.showInShade(), bubble.isSuppressed());
+            mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags());
         } catch (RemoteException e) {
             // Bad things have happened
         }
@@ -1038,7 +1037,15 @@
             }
         } else {
             Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
-            inflateAndAdd(bubble, suppressFlyout, showInShade);
+            if (notif.shouldSuppressNotificationList()) {
+                // If we're suppressing notifs for DND, we don't want the bubbles to randomly
+                // expand when DND turns off so flip the flag.
+                if (bubble.shouldAutoExpand()) {
+                    bubble.setShouldAutoExpand(false);
+                }
+            } else {
+                inflateAndAdd(bubble, suppressFlyout, showInShade);
+            }
         }
     }
 
@@ -1070,7 +1077,8 @@
         }
     }
 
-    private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
+    @VisibleForTesting
+    public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
         // shouldBubbleUp checks canBubble & for bubble metadata
         boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry);
         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
@@ -1096,7 +1104,8 @@
         }
     }
 
-    private void onRankingUpdated(RankingMap rankingMap,
+    @VisibleForTesting
+    public void onRankingUpdated(RankingMap rankingMap,
             HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
         if (mTmpRanking == null) {
             mTmpRanking = new NotificationListenerService.Ranking();
@@ -1107,19 +1116,22 @@
             Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
             BubbleEntry entry = entryData.first;
             boolean shouldBubbleUp = entryData.second;
-
             if (entry != null && !isCurrentProfile(
                     entry.getStatusBarNotification().getUser().getIdentifier())) {
                 return;
             }
-
+            if (entry != null && (entry.shouldSuppressNotificationList()
+                    || entry.getRanking().isSuspended())) {
+                shouldBubbleUp = false;
+            }
             rankingMap.getRanking(key, mTmpRanking);
-            boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
-            if (isActiveBubble && !mTmpRanking.canBubble()) {
+            boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key);
+            boolean isActive = mBubbleData.hasBubbleInStackWithKey(key);
+            if (isActiveOrInOverflow && !mTmpRanking.canBubble()) {
                 // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason.
                 // This means that the app or channel's ability to bubble has been revoked.
                 mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED);
-            } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) {
+            } else if (isActiveOrInOverflow && !shouldBubbleUp) {
                 // If this entry is allowed to bubble, but cannot currently bubble up or is
                 // suspended, dismiss it. This happens when DND is enabled and configured to hide
                 // bubbles, or focus mode is enabled and the app is designated as distracting.
@@ -1127,9 +1139,9 @@
                 // notification, so that the bubble will be re-created if shouldBubbleUp returns
                 // true.
                 mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
-            } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
+            } else if (entry != null && mTmpRanking.isBubble() && !isActive) {
                 entry.setFlagBubble(true);
-                onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended());
+                onEntryUpdated(entry, shouldBubbleUp);
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index c98c0e6..e4a0fd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -159,7 +159,7 @@
     private Listener mListener;
 
     @Nullable
-    private Bubbles.SuppressionChangedListener mSuppressionListener;
+    private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
     private Bubbles.PendingIntentCanceledListener mCancelledListener;
 
     /**
@@ -190,9 +190,8 @@
         mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
     }
 
-    public void setSuppressionChangedListener(
-            Bubbles.SuppressionChangedListener listener) {
-        mSuppressionListener = listener;
+    public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) {
+        mBubbleMetadataFlagListener = listener;
     }
 
     public void setPendingIntentCancelledListener(
@@ -311,7 +310,7 @@
                 bubbleToReturn = mPendingBubbles.get(key);
             } else if (entry != null) {
                 // New bubble
-                bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener,
+                bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener,
                         mMainExecutor);
             } else {
                 // Persisted bubble being promoted
@@ -1058,6 +1057,22 @@
         return null;
     }
 
+    /**
+     * Get a pending bubble with given notification <code>key</code>
+     *
+     * @param key notification key
+     * @return bubble that matches or null
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public Bubble getPendingBubbleWithKey(String key) {
+        for (Bubble b : mPendingBubbles.values()) {
+            if (b.getKey().equals(key)) {
+                return b;
+            }
+        }
+        return null;
+    }
+
     @VisibleForTesting(visibility = PRIVATE)
     void setTimeSource(TimeSource timeSource) {
         mTimeSource = timeSource;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 2b2a2f7..c7db8d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -263,10 +263,10 @@
         void onBubbleExpandChanged(boolean isExpanding, String key);
     }
 
-    /** Listener to be notified when the flags for notification or bubble suppression changes.*/
-    interface SuppressionChangedListener {
-        /** Called when the notification suppression state of a bubble changes. */
-        void onBubbleNotificationSuppressionChange(Bubble bubble);
+    /** Listener to be notified when the flags on BubbleMetadata have changed. */
+    interface BubbleMetadataFlagListener {
+        /** Called when the flags on BubbleMetadata have changed for the provided bubble. */
+        void onBubbleMetadataFlagChanged(Bubble bubble);
     }
 
     /** Listener to be notified when a pending intent has been canceled for a bubble. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 4b125b1..6305959 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -24,6 +24,7 @@
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.Property;
 import android.view.GestureDetector;
@@ -37,6 +38,8 @@
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
@@ -80,7 +83,6 @@
     private final Rect mTempRect = new Rect();
     private FrameLayout mDividerBar;
 
-
     static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY =
             new Property<DividerView, Integer>(Integer.class, "height") {
                 @Override
@@ -109,6 +111,74 @@
         }
     };
 
+    private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() {
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm;
+            if (isLandscape()) {
+                info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
+                        mContext.getString(R.string.accessibility_action_divider_left_full)));
+                if (snapAlgorithm.isFirstSplitTargetAvailable()) {
+                    info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
+                            mContext.getString(R.string.accessibility_action_divider_left_70)));
+                }
+                if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
+                    // Only show the middle target if there are more than 1 split target
+                    info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
+                            mContext.getString(R.string.accessibility_action_divider_left_50)));
+                }
+                if (snapAlgorithm.isLastSplitTargetAvailable()) {
+                    info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
+                            mContext.getString(R.string.accessibility_action_divider_left_30)));
+                }
+                info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
+                        mContext.getString(R.string.accessibility_action_divider_right_full)));
+            } else {
+                info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
+                        mContext.getString(R.string.accessibility_action_divider_top_full)));
+                if (snapAlgorithm.isFirstSplitTargetAvailable()) {
+                    info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
+                            mContext.getString(R.string.accessibility_action_divider_top_70)));
+                }
+                if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
+                    // Only show the middle target if there are more than 1 split target
+                    info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
+                            mContext.getString(R.string.accessibility_action_divider_top_50)));
+                }
+                if (snapAlgorithm.isLastSplitTargetAvailable()) {
+                    info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
+                            mContext.getString(R.string.accessibility_action_divider_top_30)));
+                }
+                info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
+                        mContext.getString(R.string.accessibility_action_divider_bottom_full)));
+            }
+        }
+
+        @Override
+        public boolean performAccessibilityAction(@NonNull View host, int action,
+                @Nullable Bundle args) {
+            DividerSnapAlgorithm.SnapTarget nextTarget = null;
+            DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm;
+            if (action == R.id.action_move_tl_full) {
+                nextTarget = snapAlgorithm.getDismissEndTarget();
+            } else if (action == R.id.action_move_tl_70) {
+                nextTarget = snapAlgorithm.getLastSplitTarget();
+            } else if (action == R.id.action_move_tl_50) {
+                nextTarget = snapAlgorithm.getMiddleTarget();
+            } else if (action == R.id.action_move_tl_30) {
+                nextTarget = snapAlgorithm.getFirstSplitTarget();
+            } else if (action == R.id.action_move_rb_full) {
+                nextTarget = snapAlgorithm.getDismissStartTarget();
+            }
+            if (nextTarget != null) {
+                mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), nextTarget);
+                return true;
+            }
+            return super.performAccessibilityAction(host, action, args);
+        }
+    };
+
     public DividerView(@NonNull Context context) {
         super(context);
     }
@@ -179,6 +249,7 @@
         mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener());
         mInteractive = true;
         setOnTouchListener(this);
+        mHandle.setAccessibilityDelegate(mHandleDelegate);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index 72c8141..145e527 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -29,6 +29,7 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -63,6 +64,7 @@
             Context context,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             TvPipMenuController tvPipMenuController,
             PipMediaController pipMediaController,
@@ -79,6 +81,7 @@
                         context,
                         tvPipBoundsState,
                         tvPipBoundsAlgorithm,
+                        pipAppOpsListener,
                         pipTaskOrganizer,
                         pipTransitionController,
                         tvPipMenuController,
@@ -185,4 +188,12 @@
     static PipParamsChangedForwarder providePipParamsChangedForwarder() {
         return new PipParamsChangedForwarder();
     }
+
+    @WMSingleton
+    @Provides
+    static PipAppOpsListener providePipAppOpsListener(Context context,
+            PipTaskOrganizer pipTaskOrganizer,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return new PipAppOpsListener(context, pipTaskOrganizer::removePip, mainExecutor);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 3335673..db6131a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -78,7 +78,6 @@
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipUiEventLogger;
-import com.android.wm.shell.pip.phone.PipAppOpsListener;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.recents.RecentTasksController;
@@ -435,14 +434,6 @@
         return new FloatingContentCoordinator();
     }
 
-    @WMSingleton
-    @Provides
-    static PipAppOpsListener providePipAppOpsListener(Context context,
-            PipTouchHandler pipTouchHandler,
-            @ShellMainThread ShellExecutor mainExecutor) {
-        return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor);
-    }
-
     // Needs handler for registering broadcast receivers
     @WMSingleton
     @Provides
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 7513e51..1bc9e31 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -51,6 +51,7 @@
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipMediaController;
@@ -63,9 +64,7 @@
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.pip.phone.PhonePipMenuController;
-import com.android.wm.shell.pip.phone.PipAppOpsListener;
 import com.android.wm.shell.pip.phone.PipController;
-import com.android.wm.shell.pip.phone.PipKeepClearAlgorithm;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasksController;
@@ -211,8 +210,7 @@
     @Provides
     static Optional<Pip> providePip(Context context, DisplayController displayController,
             PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState,
-            PipMotionHelper pipMotionHelper, PipMediaController pipMediaController,
+            PipBoundsState pipBoundsState, PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
             PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
             WindowManagerShellWrapper windowManagerShellWrapper,
@@ -221,8 +219,8 @@
             Optional<OneHandedController> oneHandedController,
             @ShellMainThread ShellExecutor mainExecutor) {
         return Optional.ofNullable(PipController.create(context, displayController,
-                pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState,
-                pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
+                pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState,
+                pipMediaController, phonePipMenuController, pipTaskOrganizer,
                 pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
                 taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor));
     }
@@ -241,12 +239,6 @@
 
     @WMSingleton
     @Provides
-    static PipKeepClearAlgorithm providePipKeepClearAlgorithm() {
-        return new PipKeepClearAlgorithm();
-    }
-
-    @WMSingleton
-    @Provides
     static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context,
             PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) {
         return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm);
@@ -333,6 +325,14 @@
 
     @WMSingleton
     @Provides
+    static PipAppOpsListener providePipAppOpsListener(Context context,
+            PipTouchHandler pipTouchHandler,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor);
+    }
+
+    @WMSingleton
+    @Provides
     static PipMotionHelper providePipMotionHelper(Context context,
             PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer,
             PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java
similarity index 97%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java
index d97d2d6..48a3fc2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAppOpsListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAppOpsListener.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip.phone;
+package com.android.wm.shell.pip;
 
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
@@ -28,7 +28,6 @@
 import android.util.Pair;
 
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.pip.PipUtils;
 
 public class PipAppOpsListener {
     private static final String TAG = PipAppOpsListener.class.getSimpleName();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 2e8b5b7..42ceb42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -76,6 +76,7 @@
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipMediaController;
@@ -109,9 +110,7 @@
     private PipAppOpsListener mAppOpsListener;
     private PipMediaController mMediaController;
     private PipBoundsAlgorithm mPipBoundsAlgorithm;
-    private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
     private PipBoundsState mPipBoundsState;
-    private PipMotionHelper mPipMotionHelper;
     private PipTouchHandler mTouchHandler;
     private PipTransitionController mPipTransitionController;
     private TaskStackListenerImpl mTaskStackListener;
@@ -247,10 +246,6 @@
                         Set<Rect> unrestricted) {
                     if (mPipBoundsState.getDisplayId() == displayId) {
                         mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
-                        mPipMotionHelper.moveToBounds(mPipKeepClearAlgorithm.adjust(
-                                mPipBoundsState.getBounds(),
-                                mPipBoundsState.getRestrictedKeepClearAreas(),
-                                mPipBoundsState.getUnrestrictedKeepClearAreas()));
                     }
                 }
             };
@@ -289,8 +284,7 @@
     @Nullable
     public static Pip create(Context context, DisplayController displayController,
             PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState,
-            PipMotionHelper pipMotionHelper, PipMediaController pipMediaController,
+            PipBoundsState pipBoundsState, PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
             PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
             WindowManagerShellWrapper windowManagerShellWrapper,
@@ -305,7 +299,7 @@
         }
 
         return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm,
-                pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController,
+                pipBoundsState, pipMediaController,
                 phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController,
                 windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
                 oneHandedController, mainExecutor)
@@ -316,9 +310,7 @@
             DisplayController displayController,
             PipAppOpsListener pipAppOpsListener,
             PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm,
             @NonNull PipBoundsState pipBoundsState,
-            PipMotionHelper pipMotionHelper,
             PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController,
             PipTaskOrganizer pipTaskOrganizer,
@@ -341,9 +333,7 @@
         mWindowManagerShellWrapper = windowManagerShellWrapper;
         mDisplayController = displayController;
         mPipBoundsAlgorithm = pipBoundsAlgorithm;
-        mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
         mPipBoundsState = pipBoundsState;
-        mPipMotionHelper = pipMotionHelper;
         mPipTaskOrganizer = pipTaskOrganizer;
         mMainExecutor = mainExecutor;
         mMediaController = pipMediaController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithm.java
deleted file mode 100644
index a83258f..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithm.java
+++ /dev/null
@@ -1,44 +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.wm.shell.pip.phone;
-
-import android.graphics.Rect;
-
-import java.util.Set;
-
-/**
- * Calculates the adjusted position that does not occlude keep clear areas.
- */
-public class PipKeepClearAlgorithm {
-
-    /** Returns a new {@code Rect} that does not occlude the provided keep clear areas. */
-    public Rect adjust(Rect defaultBounds, Set<Rect> restrictedKeepClearAreas,
-            Set<Rect> unrestrictedKeepClearAreas) {
-        if (restrictedKeepClearAreas.isEmpty()) {
-            return defaultBounds;
-        }
-        // TODO(b/183746978): implement the adjustment algorithm
-        // naively check if areas intersect, an if so move PiP upwards
-        Rect outBounds = new Rect(defaultBounds);
-        for (Rect r : restrictedKeepClearAreas) {
-            if (r.intersect(outBounds)) {
-                outBounds.offset(0, r.top - outBounds.bottom);
-            }
-        }
-        return outBounds;
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index e9b6bab..5a21e07 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -44,6 +44,7 @@
 import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 8326588..fcd1f95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -45,6 +45,7 @@
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
@@ -97,6 +98,7 @@
 
     private final TvPipBoundsState mTvPipBoundsState;
     private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
+    private final PipAppOpsListener mAppOpsListener;
     private final PipTaskOrganizer mPipTaskOrganizer;
     private final PipMediaController mPipMediaController;
     private final TvPipNotificationController mPipNotificationController;
@@ -121,6 +123,7 @@
             Context context,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             PipTransitionController pipTransitionController,
             TvPipMenuController tvPipMenuController,
@@ -136,6 +139,7 @@
                 context,
                 tvPipBoundsState,
                 tvPipBoundsAlgorithm,
+                pipAppOpsListener,
                 pipTaskOrganizer,
                 pipTransitionController,
                 tvPipMenuController,
@@ -153,6 +157,7 @@
             Context context,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             PipTransitionController pipTransitionController,
             TvPipMenuController tvPipMenuController,
@@ -181,6 +186,7 @@
         mTvPipMenuController = tvPipMenuController;
         mTvPipMenuController.setDelegate(this);
 
+        mAppOpsListener = pipAppOpsListener;
         mPipTaskOrganizer = pipTaskOrganizer;
         pipTransitionController.registerPipTransitionCallback(this);
 
@@ -521,6 +527,12 @@
             @Override
             public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
                 checkIfPinnedTaskAppeared();
+                mAppOpsListener.onActivityPinned(packageName);
+            }
+
+            @Override
+            public void onActivityUnpinned() {
+                mAppOpsListener.onActivityUnpinned();
             }
 
             @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 868e456..320c05c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -494,6 +494,14 @@
         setFrameHighlighted(false);
     }
 
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (!hasWindowFocus) {
+            hideAllUserControls();
+        }
+    }
+
     private void animateAlphaTo(float alpha, View view) {
         if (view.getAlpha() == alpha) {
             return;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 169f03e..bde94d9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -115,7 +115,7 @@
     private ArgumentCaptor<BubbleData.Update> mUpdateCaptor;
 
     @Mock
-    private Bubbles.SuppressionChangedListener mSuppressionListener;
+    private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
 
     @Mock
     private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener;
@@ -136,30 +136,47 @@
                 mock(NotificationListenerService.Ranking.class);
         when(ranking.isTextChanged()).thenReturn(true);
         mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking);
-        mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null,
+        mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null,
                 mMainExecutor);
 
         mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null);
-        mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null,
+        mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null,
                 mMainExecutor);
 
         mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null,
                 new LocusId("locusId1"));
-        mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor);
+        mBubbleLocusId = new Bubble(mEntryLocusId,
+                mBubbleMetadataFlagListener,
+                null /* pendingIntentCanceledListener */,
+                mMainExecutor);
 
-        mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleA1 = new Bubble(mEntryA1,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
-        mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleA2 = new Bubble(mEntryA2,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
-        mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleA3 = new Bubble(mEntryA3,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
-        mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleB1 = new Bubble(mEntryB1,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
-        mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleB2 = new Bubble(mEntryB2,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
-        mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleB3 = new Bubble(mEntryB3,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
-        mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener,
+        mBubbleC1 = new Bubble(mEntryC1,
+                mBubbleMetadataFlagListener,
+                mPendingIntentCanceledListener,
                 mMainExecutor);
         mPositioner = new TestableBubblePositioner(mContext,
                 mock(WindowManager.class));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
index 819a984..e8f3f69 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
@@ -63,7 +63,7 @@
     private Bubble mBubble;
 
     @Mock
-    private Bubbles.SuppressionChangedListener mSuppressionListener;
+    private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
 
     @Before
     public void setUp() {
@@ -81,7 +81,7 @@
         when(mNotif.getBubbleMetadata()).thenReturn(metadata);
         when(mSbn.getKey()).thenReturn("mock");
         mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false);
-        mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor);
+        mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor);
     }
 
     @Test
@@ -144,22 +144,22 @@
     }
 
     @Test
-    public void testSuppressionListener_change_notified() {
+    public void testBubbleMetadataFlagListener_change_notified() {
         assertThat(mBubble.showInShade()).isTrue();
 
         mBubble.setSuppressNotification(true);
 
         assertThat(mBubble.showInShade()).isFalse();
 
-        verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble);
+        verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble);
     }
 
     @Test
-    public void testSuppressionListener_noChange_doesntNotify() {
+    public void testBubbleMetadataFlagListener_noChange_doesntNotify() {
         assertThat(mBubble.showInShade()).isTrue();
 
         mBubble.setSuppressNotification(false);
 
-        verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any());
+        verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any());
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 5368b7d..df18133 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -45,6 +45,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipMediaController;
@@ -75,7 +76,6 @@
     @Mock private PhonePipMenuController mMockPhonePipMenuController;
     @Mock private PipAppOpsListener mMockPipAppOpsListener;
     @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm;
-    @Mock private PipKeepClearAlgorithm mMockPipKeepClearAlgorithm;
     @Mock private PipSnapAlgorithm mMockPipSnapAlgorithm;
     @Mock private PipMediaController mMockPipMediaController;
     @Mock private PipTaskOrganizer mMockPipTaskOrganizer;
@@ -100,12 +100,12 @@
             return null;
         }).when(mMockExecutor).execute(any());
         mPipController = new PipController(mContext, mMockDisplayController,
-                mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
-                mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
+                mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
+                mMockPipBoundsState, mMockPipMediaController,
                 mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler,
                 mMockPipTransitionController, mMockWindowManagerShellWrapper,
-                mMockTaskStackListener, mPipParamsChangedForwarder, mMockOneHandedController,
-                mMockExecutor);
+                mMockTaskStackListener, mPipParamsChangedForwarder,
+                mMockOneHandedController, mMockExecutor);
         when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
         when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
     }
@@ -133,12 +133,12 @@
         when(spyContext.getPackageManager()).thenReturn(mockPackageManager);
 
         assertNull(PipController.create(spyContext, mMockDisplayController,
-                mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
-                mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
+                mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
+                mMockPipBoundsState, mMockPipMediaController,
                 mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler,
                 mMockPipTransitionController, mMockWindowManagerShellWrapper,
-                mMockTaskStackListener, mPipParamsChangedForwarder, mMockOneHandedController,
-                mMockExecutor));
+                mMockTaskStackListener, mPipParamsChangedForwarder,
+                mMockOneHandedController, mMockExecutor));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithmTest.java
deleted file mode 100644
index f657b5e..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipKeepClearAlgorithmTest.java
+++ /dev/null
@@ -1,96 +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.wm.shell.pip.phone;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
-import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Set;
-
-/**
- * Unit tests against {@link PipKeepClearAlgorithm}.
- */
-@RunWith(AndroidTestingRunner.class)
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class PipKeepClearAlgorithmTest extends ShellTestCase {
-
-    private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
-
-
-    @Before
-    public void setUp() throws Exception {
-        mPipKeepClearAlgorithm = new PipKeepClearAlgorithm();
-    }
-
-    @Test
-    public void adjust_withCollidingRestrictedKeepClearAreas_movesBounds() {
-        final Rect inBounds = new Rect(0, 0, 100, 100);
-        final Rect keepClearRect = new Rect(50, 50, 150, 150);
-
-        final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(keepClearRect),
-                Set.of());
-
-        assertFalse(outBounds.contains(keepClearRect));
-    }
-
-    @Test
-    public void adjust_withNonCollidingRestrictedKeepClearAreas_boundsDoNotChange() {
-        final Rect inBounds = new Rect(0, 0, 100, 100);
-        final Rect keepClearRect = new Rect(100, 100, 150, 150);
-
-        final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(keepClearRect),
-                Set.of());
-
-        assertEquals(inBounds, outBounds);
-    }
-
-    @Test
-    public void adjust_withCollidingUnrestrictedKeepClearAreas_boundsDoNotChange() {
-        // TODO(b/183746978): update this test to accommodate for the updated algorithm
-        final Rect inBounds = new Rect(0, 0, 100, 100);
-        final Rect keepClearRect = new Rect(50, 50, 150, 150);
-
-        final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(),
-                Set.of(keepClearRect));
-
-        assertEquals(inBounds, outBounds);
-    }
-
-    @Test
-    public void adjust_withNonCollidingUnrestrictedKeepClearAreas_boundsDoNotChange() {
-        final Rect inBounds = new Rect(0, 0, 100, 100);
-        final Rect keepClearRect = new Rect(100, 100, 150, 150);
-
-        final Rect outBounds = mPipKeepClearAlgorithm.adjust(inBounds, Set.of(),
-                Set.of(keepClearRect));
-
-        assertEquals(inBounds, outBounds);
-    }
-}
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index e7432ac..90c4440 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -136,24 +136,59 @@
         free(valueBuffer);
         return nullptr;
     }
+    mNumShadersCachedInRam++;
+    ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam);
     return SkData::MakeFromMalloc(valueBuffer, valueSize);
 }
 
+namespace {
+// Helper for BlobCache::set to trace the result.
+void set(BlobCache* cache, const void* key, size_t keySize, const void* value, size_t valueSize) {
+    switch (cache->set(key, keySize, value, valueSize)) {
+        case BlobCache::InsertResult::kInserted:
+            // This is what we expect/hope. It means the cache is large enough.
+            return;
+        case BlobCache::InsertResult::kDidClean: {
+            ATRACE_FORMAT("ShaderCache: evicted an entry to fit {key: %lu value %lu}!", keySize,
+                          valueSize);
+            return;
+        }
+        case BlobCache::InsertResult::kNotEnoughSpace: {
+            ATRACE_FORMAT("ShaderCache: could not fit {key: %lu value %lu}!", keySize, valueSize);
+            return;
+        }
+        case BlobCache::InsertResult::kInvalidValueSize:
+        case BlobCache::InsertResult::kInvalidKeySize: {
+            ATRACE_FORMAT("ShaderCache: invalid size {key: %lu value %lu}!", keySize, valueSize);
+            return;
+        }
+        case BlobCache::InsertResult::kKeyTooBig:
+        case BlobCache::InsertResult::kValueTooBig:
+        case BlobCache::InsertResult::kCombinedTooBig: {
+            ATRACE_FORMAT("ShaderCache: entry too big: {key: %lu value %lu}!", keySize, valueSize);
+            return;
+        }
+    }
+}
+}  // namespace
+
 void ShaderCache::saveToDiskLocked() {
     ATRACE_NAME("ShaderCache::saveToDiskLocked");
     if (mInitialized && mBlobCache && mSavePending) {
         if (mIDHash.size()) {
             auto key = sIDKey;
-            mBlobCache->set(&key, sizeof(key), mIDHash.data(), mIDHash.size());
+            set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size());
         }
         mBlobCache->writeToFile();
     }
     mSavePending = false;
 }
 
-void ShaderCache::store(const SkData& key, const SkData& data) {
+void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) {
     ATRACE_NAME("ShaderCache::store");
     std::lock_guard<std::mutex> lock(mMutex);
+    mNumShadersCachedInRam++;
+    ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam);
 
     if (!mInitialized) {
         return;
@@ -187,7 +222,7 @@
         mNewPipelineCacheSize = -1;
         mTryToStorePipelineCache = true;
     }
-    bc->set(key.data(), keySize, value, valueSize);
+    set(bc, key.data(), keySize, value, valueSize);
 
     if (!mSavePending && mDeferredSaveDelay > 0) {
         mSavePending = true;
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index 4dcc9fb..3e0fd51 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -73,7 +73,7 @@
      * "store" attempts to insert a new key/value blob pair into the cache.
      * This will be called by Skia after it compiled a new SKSL shader
      */
-    void store(const SkData& key, const SkData& data) override;
+    void store(const SkData& key, const SkData& data, const SkString& description) override;
 
     /**
      * "onVkFrameFlushed" tries to store Vulkan pipeline cache state.
@@ -210,6 +210,13 @@
      */
     static constexpr uint8_t sIDKey = 0;
 
+    /**
+     * Most of this class concerns persistent storage for shaders, but it's also
+     * interesting to keep track of how many shaders are stored in RAM. This
+     * class provides a convenient entry point for that.
+     */
+    int mNumShadersCachedInRam = 0;
+
     friend class ShaderCacheTestUtils;  // used for unit testing
 };
 
diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp
index 87981f1..974d85a 100644
--- a/libs/hwui/tests/unit/ShaderCacheTests.cpp
+++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp
@@ -140,9 +140,9 @@
     // write to the in-memory cache without storing on disk and verify we read the same values
     sk_sp<SkData> inVS;
     setShader(inVS, "sassas");
-    ShaderCache::get().store(GrProgramDescTest(100), *inVS.get());
+    ShaderCache::get().store(GrProgramDescTest(100), *inVS.get(), SkString());
     setShader(inVS, "someVS");
-    ShaderCache::get().store(GrProgramDescTest(432), *inVS.get());
+    ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString());
     ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(100))), sk_sp<SkData>());
     ASSERT_TRUE(checkShader(outVS, "sassas"));
     ASSERT_NE((outVS = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>());
@@ -166,7 +166,7 @@
 
     // change data, store to disk, read back again and verify data has been changed
     setShader(inVS, "ewData1");
-    ShaderCache::get().store(GrProgramDescTest(432), *inVS.get());
+    ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString());
     ShaderCacheTestUtils::terminate(ShaderCache::get(), true);
     ShaderCache::get().initShaderDiskCache();
     ASSERT_NE((outVS2 = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>());
@@ -177,7 +177,7 @@
     std::vector<uint8_t> dataBuffer(dataSize);
     genRandomData(dataBuffer);
     setShader(inVS, dataBuffer);
-    ShaderCache::get().store(GrProgramDescTest(432), *inVS.get());
+    ShaderCache::get().store(GrProgramDescTest(432), *inVS.get(), SkString());
     ShaderCacheTestUtils::terminate(ShaderCache::get(), true);
     ShaderCache::get().initShaderDiskCache();
     ASSERT_NE((outVS2 = ShaderCache::get().load(GrProgramDescTest(432))), sk_sp<SkData>());
@@ -225,7 +225,7 @@
         setShader(data, dataBuffer);
 
         blob = std::make_pair(key, data);
-        ShaderCache::get().store(*key.get(), *data.get());
+        ShaderCache::get().store(*key.get(), *data.get(), SkString());
     }
     ShaderCacheTestUtils::terminate(ShaderCache::get(), true);
 
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 1dc74e5..10ea651 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -106,6 +106,7 @@
 PointerController::~PointerController() {
     mDisplayInfoListener->onPointerControllerDestroyed();
     mUnregisterWindowInfosListener(mDisplayInfoListener);
+    mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, 0, 0);
 }
 
 std::mutex& PointerController::getLock() const {
@@ -255,6 +256,12 @@
         getAdditionalMouseResources = true;
     }
     mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
+    if (viewport.displayId != mLocked.pointerDisplayId) {
+        float xPos, yPos;
+        mCursorController.getPosition(&xPos, &yPos);
+        mContext.getPolicy()->onPointerDisplayIdChanged(viewport.displayId, xPos, yPos);
+        mLocked.pointerDisplayId = viewport.displayId;
+    }
 }
 
 void PointerController::updatePointerIcon(int32_t iconId) {
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 2e6e851..eab030f 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -104,6 +104,7 @@
 
     struct Locked {
         Presentation presentation;
+        int32_t pointerDisplayId = ADISPLAY_ID_NONE;
 
         std::vector<gui::DisplayInfo> mDisplayInfos;
         std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers;
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index 26a65a4..c2bc1e0 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -79,6 +79,7 @@
             std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) = 0;
     virtual int32_t getDefaultPointerIconId() = 0;
     virtual int32_t getCustomPointerIconId() = 0;
+    virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0;
 };
 
 /*
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index dae1fcc..f9752ed 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -56,9 +56,11 @@
             std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId) override;
     virtual int32_t getDefaultPointerIconId() override;
     virtual int32_t getCustomPointerIconId() override;
+    virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override;
 
     bool allResourcesAreLoaded();
     bool noResourcesAreLoaded();
+    std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; }
 
 private:
     void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType);
@@ -66,6 +68,7 @@
     bool pointerIconLoaded{false};
     bool pointerResourcesLoaded{false};
     bool additionalMouseResourcesLoaded{false};
+    std::optional<int32_t /*displayId*/> latestPointerDisplayId;
 };
 
 void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) {
@@ -126,12 +129,19 @@
     icon->hotSpotX = hotSpot.first;
     icon->hotSpotY = hotSpot.second;
 }
+
+void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId,
+                                                                     float /*xPos*/,
+                                                                     float /*yPos*/) {
+    latestPointerDisplayId = displayId;
+}
+
 class PointerControllerTest : public Test {
 protected:
     PointerControllerTest();
     ~PointerControllerTest();
 
-    void ensureDisplayViewportIsSet();
+    void ensureDisplayViewportIsSet(int32_t displayId = ADISPLAY_ID_DEFAULT);
 
     sp<MockSprite> mPointerSprite;
     sp<MockPointerControllerPolicyInterface> mPolicy;
@@ -168,9 +178,9 @@
     mThread.join();
 }
 
-void PointerControllerTest::ensureDisplayViewportIsSet() {
+void PointerControllerTest::ensureDisplayViewportIsSet(int32_t displayId) {
     DisplayViewport viewport;
-    viewport.displayId = ADISPLAY_ID_DEFAULT;
+    viewport.displayId = displayId;
     viewport.logicalRight = 1600;
     viewport.logicalBottom = 1200;
     viewport.physicalRight = 800;
@@ -255,6 +265,30 @@
     ensureDisplayViewportIsSet();
 }
 
+TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) {
+    EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId())
+            << "A pointer display change does not occur when PointerController is created.";
+
+    ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT);
+
+    const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId();
+    ASSERT_TRUE(lastReportedPointerDisplayId)
+            << "The policy is notified of a pointer display change when the viewport is first set.";
+    EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId)
+            << "Incorrect pointer display notified.";
+
+    ensureDisplayViewportIsSet(42);
+
+    EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId())
+            << "The policy is notified when the pointer display changes.";
+
+    // Release the PointerController.
+    mPointerController = nullptr;
+
+    EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId())
+            << "The pointer display changes to invalid when PointerController is destroyed.";
+}
+
 class PointerControllerWindowInfoListenerTest : public Test {};
 
 class TestPointerController : public PointerController {
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index e8a1a5c..f333b86 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -150,8 +150,6 @@
         mBtAdapter = mBtManager.getAdapter();
         mBleScanner = mBtAdapter.getBluetoothLeScanner();
         mWifiManager = getSystemService(WifiManager.class);
-
-        sScanResultsLiveData.setValue(Collections.emptyList());
     }
 
     @Override
@@ -175,6 +173,7 @@
 
     @Override
     public void onDestroy() {
+        sScanResultsLiveData.setValue(Collections.emptyList());
         super.onDestroy();
         if (DEBUG) Log.d(TAG, "onDestroy()");
     }
@@ -188,6 +187,7 @@
         mStopAfterFirstMatch = request.isSingleDevice();
         mDiscoveryStarted = true;
         sStateLiveData.setValue(DiscoveryState.DISCOVERY_IN_PROGRESS);
+        sScanResultsLiveData.setValue(Collections.emptyList());
 
         final List<DeviceFilter<?>> allFilters = request.getDeviceFilters();
         final List<BluetoothDeviceFilter> btFilters =
diff --git a/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml b/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml
index 212ae52..42700b3 100644
--- a/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml
+++ b/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml
@@ -60,6 +60,7 @@
             android:text="@string/settingslib_learn_more_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:paddingBottom="8dp"
             android:clickable="true"
             android:visibility="gone"
             style="@style/TextAppearance.Footer.Title.SettingsLib"/>
diff --git a/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml b/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml
index d403f9e..1adcead 100644
--- a/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml
+++ b/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml
@@ -59,6 +59,7 @@
             android:text="@string/settingslib_learn_more_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:paddingBottom="8dp"
             android:clickable="true"
             android:visibility="gone"
             style="@style/TextAppearance.Footer.Title.SettingsLib"/>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
index bdeb474..e6160bb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
@@ -16,10 +16,14 @@
 
 package com.android.settingslib;
 
+import static android.app.admin.DevicePolicyResources.Strings.Settings.DISABLED_BY_ADMIN_SWITCH_SUMMARY;
+import static android.app.admin.DevicePolicyResources.Strings.Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY;
+
 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
 import android.annotation.NonNull;
 import android.app.AppOpsManager;
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.os.Process;
@@ -37,6 +41,8 @@
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.SwitchPreference;
 
+import com.android.settingslib.utils.BuildCompatUtils;
+
 /**
  * Version of SwitchPreference that can be disabled by a device admin
  * using a user restriction.
@@ -117,8 +123,13 @@
 
         CharSequence switchSummary;
         if (mRestrictedSwitchSummary == null) {
-            switchSummary = getContext().getText(isChecked()
-                    ? R.string.enabled_by_admin : R.string.disabled_by_admin);
+            switchSummary = isChecked()
+                    ? getUpdatableEnterpriseString(
+                            getContext(), ENABLED_BY_ADMIN_SWITCH_SUMMARY,
+                            R.string.enabled_by_admin)
+                    : getUpdatableEnterpriseString(
+                            getContext(), DISABLED_BY_ADMIN_SWITCH_SUMMARY,
+                            R.string.disabled_by_admin);
         } else {
             switchSummary = mRestrictedSwitchSummary;
         }
@@ -153,6 +164,16 @@
         }
     }
 
+    private static String getUpdatableEnterpriseString(
+            Context context, String updatableStringId, int resId) {
+        if (!BuildCompatUtils.isAtLeastT()) {
+            return context.getString(resId);
+        }
+        return context.getSystemService(DevicePolicyManager.class).getResources().getString(
+                updatableStringId,
+                () -> context.getString(resId));
+    }
+
     @Override
     public void performClick() {
         if (!mHelper.performClick()) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
index c248fff..b0392be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
@@ -26,6 +26,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothLeBroadcastAssistant;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothProfile.ServiceListener;
 import android.content.Context;
@@ -202,6 +203,43 @@
         mService.startSearchingForSources(filters);
     }
 
+    /**
+     * Return true if a search has been started by this application.
+     *
+     * @return true if a search has been started by this application
+     * @hide
+     */
+    public boolean isSearchInProgress() {
+        if (DEBUG) {
+            Log.d(TAG, "isSearchInProgress()");
+        }
+        if (mService == null) {
+            Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
+            return false;
+        }
+        return mService.isSearchInProgress();
+    }
+
+    /**
+     * Get information about all Broadcast Sources that a Broadcast Sink knows about.
+     *
+     * @param sink Broadcast Sink from which to get all Broadcast Sources
+     * @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState}
+     *         stored in the Broadcast Sink
+     * @throws NullPointerException when <var>sink</var> is null
+     */
+    public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources(
+            @NonNull BluetoothDevice sink) {
+        if (DEBUG) {
+            Log.d(TAG, "getAllSources()");
+        }
+        if (mService == null) {
+            Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
+            return new ArrayList<BluetoothLeBroadcastReceiveState>();
+        }
+        return mService.getAllSources(sink);
+    }
+
     public void registerServiceCallBack(@NonNull @CallbackExecutor Executor executor,
             @NonNull BluetoothLeBroadcastAssistant.Callback callback) {
         if (mService == null) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java
index 5b1fefb..aff9a6e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java
@@ -25,6 +25,7 @@
 import android.bluetooth.BluetoothLeBroadcastSubgroup;
 import android.util.Log;
 
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -95,6 +96,7 @@
     }
 
     public String convertToQrCodeString() {
+        String subgroupString = convertSubgroupToString(mSubgroupList);
         return new StringBuilder()
                 .append(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)
                 .append(BluetoothBroadcastUtils.PREFIX_BT_ADDRESS_TYPE)
@@ -122,11 +124,83 @@
                 .append(METADATA_START).append(mPresentationDelayMicros).append(METADATA_END)
                 .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
                 .append(BluetoothBroadcastUtils.PREFIX_BT_SUBGROUPS)
-                .append(METADATA_START).append(mSubgroupList).append(METADATA_END)
+                .append(METADATA_START).append(subgroupString).append(METADATA_END)
                 .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
                 .toString();
     }
 
+    private String convertSubgroupToString(List<BluetoothLeBroadcastSubgroup> subgroupList) {
+        StringBuilder subgroupListBuilder = new StringBuilder();
+        String subgroupString = "";
+        for (BluetoothLeBroadcastSubgroup subgroup: subgroupList) {
+            String audioCodec = convertAudioCodecConfigToString(subgroup.getCodecSpecificConfig());
+            String audioContent = convertAudioContentToString(subgroup.getContentMetadata());
+            String channels = convertChannelToString(subgroup.getChannels());
+            subgroupString = new StringBuilder()
+                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_CODEC_ID)
+                    .append(METADATA_START).append(subgroup.getCodecId()).append(METADATA_END)
+                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_CODEC_CONFIG)
+                    .append(METADATA_START).append(audioCodec).append(METADATA_END)
+                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_AUDIO_CONTENT)
+                    .append(METADATA_START).append(audioContent).append(METADATA_END)
+                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+                    .append(BluetoothBroadcastUtils.PREFIX_BTSG_BROADCAST_CHANNEL)
+                    .append(METADATA_START).append(channels).append(METADATA_END)
+                    .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+                    .toString();
+            subgroupListBuilder.append(subgroupString);
+        }
+        return subgroupListBuilder.toString();
+    }
+
+    private String convertAudioCodecConfigToString(BluetoothLeAudioCodecConfigMetadata config) {
+        String audioLocation = String.valueOf(config.getAudioLocation());
+        String rawMetadata = new String(config.getRawMetadata(), StandardCharsets.UTF_8);
+        return new StringBuilder()
+            .append(BluetoothBroadcastUtils.PREFIX_BTCC_AUDIO_LOCATION)
+            .append(METADATA_START).append(audioLocation).append(METADATA_END)
+            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+            .append(BluetoothBroadcastUtils.PREFIX_BTCC_RAW_METADATA)
+            .append(METADATA_START).append(rawMetadata).append(METADATA_END)
+            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+            .toString();
+    }
+
+    private String convertAudioContentToString(BluetoothLeAudioContentMetadata audioContent) {
+        String rawMetadata = new String(audioContent.getRawMetadata(), StandardCharsets.UTF_8);
+        return new StringBuilder()
+            .append(BluetoothBroadcastUtils.PREFIX_BTAC_PROGRAM_INFO)
+            .append(METADATA_START).append(audioContent.getProgramInfo()).append(METADATA_END)
+            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+            .append(BluetoothBroadcastUtils.PREFIX_BTAC_LANGUAGE)
+            .append(METADATA_START).append(audioContent.getLanguage()).append(METADATA_END)
+            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+            .append(BluetoothBroadcastUtils.PREFIX_BTAC_RAW_METADATA)
+            .append(METADATA_START).append(rawMetadata).append(METADATA_END)
+            .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+            .toString();
+    }
+
+    private String convertChannelToString(List<BluetoothLeBroadcastChannel> channelList) {
+        StringBuilder channelListBuilder = new StringBuilder();
+        String channelString = "";
+        for (BluetoothLeBroadcastChannel channel: channelList) {
+            String channelAudioCodec = convertAudioCodecConfigToString(channel.getCodecMetadata());
+            channelString = new StringBuilder()
+                .append(BluetoothBroadcastUtils.PREFIX_BTBC_CHANNEL_INDEX)
+                .append(METADATA_START).append(channel.getChannelIndex()).append(METADATA_END)
+                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+                .append(BluetoothBroadcastUtils.PREFIX_BTBC_CODEC_CONFIG)
+                .append(METADATA_START).append(channelAudioCodec).append(METADATA_END)
+                .append(BluetoothBroadcastUtils.DELIMITER_QR_CODE)
+                .toString();
+            channelListBuilder.append(channelString);
+        }
+        return channelListBuilder.toString();
+    }
+
     /**
      * Example : prefix is with the “BT:”, and end by the Android Version.
      * BT:T:<1>;D:<00:11:22:AA:BB:CC>;AS:<1>;B:…;V:T;;
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
index 20fe495..988055e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
@@ -1,9 +1,13 @@
 package com.android.settingslib.core;
 
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.os.Build;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceScreen;
@@ -16,9 +20,12 @@
     private static final String TAG = "AbstractPrefController";
 
     protected final Context mContext;
+    private final DevicePolicyManager mDevicePolicyManager;
 
     public AbstractPreferenceController(Context context) {
         mContext = context;
+        mDevicePolicyManager =
+                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
     }
 
     /**
@@ -102,4 +109,40 @@
     public CharSequence getSummary() {
         return null;
     }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    protected void replaceEnterpriseStringTitle(PreferenceScreen screen,
+            String preferenceKey, String overrideKey, int resource) {
+        if (!BuildCompat.isAtLeastT() || mDevicePolicyManager == null) {
+            return;
+        }
+
+        Preference preference = screen.findPreference(preferenceKey);
+        if (preference == null) {
+            Log.d(TAG, "Could not find enterprise preference " + preferenceKey);
+            return;
+        }
+
+        preference.setTitle(
+                mDevicePolicyManager.getResources().getString(overrideKey,
+                        () -> mContext.getString(resource)));
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    protected void replaceEnterpriseStringSummary(
+            PreferenceScreen screen, String preferenceKey, String overrideKey, int resource) {
+        if (!BuildCompat.isAtLeastT() || mDevicePolicyManager == null) {
+            return;
+        }
+
+        Preference preference = screen.findPreference(preferenceKey);
+        if (preference == null) {
+            Log.d(TAG, "Could not find enterprise preference " + preferenceKey);
+            return;
+        }
+
+        preference.setSummary(
+                mDevicePolicyManager.getResources().getString(overrideKey,
+                        () -> mContext.getString(resource)));
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java
index 1b63e3e..1b0738f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java
@@ -16,12 +16,16 @@
 
 package com.android.settingslib;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.DevicePolicyResourcesManager;
 import android.content.Context;
 import android.view.View;
 import android.widget.TextView;
@@ -30,7 +34,6 @@
 import androidx.preference.PreferenceViewHolder;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -45,6 +48,10 @@
     @Mock
     private Preference mPreference;
     @Mock
+    private DevicePolicyManager mDevicePolicyManager;
+    @Mock
+    private DevicePolicyResourcesManager mDevicePolicyResourcesManager;
+    @Mock
     private RestrictedTopLevelPreference mRestrictedTopLevelPreference;
 
     private PreferenceViewHolder mViewHolder;
@@ -53,18 +60,22 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        doReturn(mDevicePolicyResourcesManager).when(mDevicePolicyManager)
+                .getResources();
+        doReturn(mDevicePolicyManager).when(mContext)
+                .getSystemService(DevicePolicyManager.class);
         mViewHolder = PreferenceViewHolder.createInstanceForTests(mock(View.class));
         mHelper = new RestrictedPreferenceHelper(mContext, mPreference, null);
     }
 
     @Test
-    @Ignore
     public void bindPreference_disabled_shouldDisplayDisabledSummary() {
         final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS);
         when(mViewHolder.itemView.findViewById(android.R.id.summary))
                 .thenReturn(summaryView);
         when(summaryView.getContext().getText(R.string.disabled_by_admin_summary_text))
                 .thenReturn("test");
+        when(mDevicePolicyResourcesManager.getString(any(), any())).thenReturn("test");
 
         mHelper.useAdminDisabledSummary(true);
         mHelper.setDisabledByAdmin(new RestrictedLockUtils.EnforcedAdmin());
@@ -75,13 +86,13 @@
     }
 
     @Test
-    @Ignore
     public void bindPreference_notDisabled_shouldNotHideSummary() {
         final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS);
         when(mViewHolder.itemView.findViewById(android.R.id.summary))
                 .thenReturn(summaryView);
         when(summaryView.getContext().getText(R.string.disabled_by_admin_summary_text))
                 .thenReturn("test");
+        when(mDevicePolicyResourcesManager.getString(any(), any())).thenReturn("test");
         when(summaryView.getText()).thenReturn("test");
 
         mHelper.useAdminDisabledSummary(true);
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml
index 9694999..c4baa52 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp-land/dimens.xml
@@ -22,6 +22,4 @@
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">88dp</dimen>
 
-    <dimen name="qs_panel_padding_top">16dp</dimen>
-
-</resources>
\ No newline at end of file
+</resources>
diff --git a/packages/SystemUI/res-product/values/strings.xml b/packages/SystemUI/res-product/values/strings.xml
index c1e81ba..b71caef 100644
--- a/packages/SystemUI/res-product/values/strings.xml
+++ b/packages/SystemUI/res-product/values/strings.xml
@@ -130,4 +130,8 @@
     <!-- Text shown when viewing global actions while phone is locked and additional controls are hidden [CHAR LIMIT=NONE] -->
     <string name="global_action_lock_message" product="device">Unlock your device for more options</string>
 
+    <!-- Text informing the user that their media is now playing on this device. [CHAR LIMIT=50] -->
+    <string name="media_transfer_playing_this_device" product="default">Playing on this phone</string>
+    <!-- Text informing the user that their media is now playing on this tablet device. [CHAR LIMIT=50] -->
+    <string name="media_transfer_playing_this_device" product="tablet">Playing on this tablet</string>
 </resources>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index b230438..10bb6cb 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -121,6 +121,30 @@
             android:adjustViewBounds="true"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
+        <TextView
+            android:id="@+id/hidden_text_preview"
+            android:visibility="gone"
+            android:textFontWeight="500"
+            android:padding="8dp"
+            android:gravity="center"
+            android:textSize="14sp"
+            android:text="@string/clipboard_text_hidden"
+            android:textColor="?attr/overlayButtonTextColor"
+            android:background="?androidprv:attr/colorAccentSecondary"
+            android:layout_width="@dimen/clipboard_preview_size"
+            android:layout_height="@dimen/clipboard_preview_size"/>
+        <TextView
+            android:id="@+id/hidden_image_preview"
+            android:visibility="gone"
+            android:textFontWeight="500"
+            android:padding="8dp"
+            android:gravity="center"
+            android:textSize="14sp"
+            android:text="@string/clipboard_text_hidden"
+            android:textColor="#ffffff"
+            android:background="#000000"
+            android:layout_width="@dimen/clipboard_preview_size"
+            android:layout_height="@dimen/clipboard_preview_size"/>
     </FrameLayout>
     <FrameLayout
         android:id="@+id/dismiss_button"
@@ -141,4 +165,4 @@
             android:layout_margin="@dimen/overlay_dismiss_button_margin"
             android:src="@drawable/overlay_cancel"/>
     </FrameLayout>
-</com.android.systemui.screenshot.DraggableConstraintLayout>
+</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/fgs_manager_app_item.xml b/packages/SystemUI/res/layout/fgs_manager_app_item.xml
index 30bce9d..a6f659d 100644
--- a/packages/SystemUI/res/layout/fgs_manager_app_item.xml
+++ b/packages/SystemUI/res/layout/fgs_manager_app_item.xml
@@ -50,7 +50,8 @@
   <Button
       android:id="@+id/fgs_manager_app_item_stop_button"
       android:layout_width="wrap_content"
-      android:layout_height="48dp"
+      android:layout_height="wrap_content"
+      android:minHeight="48dp"
       android:text="@string/fgs_manager_app_item_stop_button_label"
       android:layout_marginStart="12dp"
       style="?android:attr/buttonBarNeutralButtonStyle" />
diff --git a/packages/SystemUI/res/layout/qs_paged_page.xml b/packages/SystemUI/res/layout/qs_paged_page.xml
index 98804fc..c366ceb 100644
--- a/packages/SystemUI/res/layout/qs_paged_page.xml
+++ b/packages/SystemUI/res/layout/qs_paged_page.xml
@@ -19,7 +19,7 @@
     android:id="@+id/tile_page"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingStart="@dimen/notification_side_paddings"
-    android:paddingEnd="@dimen/notification_side_paddings"
+    android:paddingStart="@dimen/qs_tiles_page_horizontal_margin"
+    android:paddingEnd="@dimen/qs_tiles_page_horizontal_margin"
     android:clipChildren="false"
     android:clipToPadding="false" />
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index b3da144..6c42073 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -21,13 +21,20 @@
     <dimen name="keyguard_indication_margin_bottom">25dp</dimen>
     <dimen name="ambient_indication_margin_bottom">115dp</dimen>
     <dimen name="lock_icon_margin_bottom">60dp</dimen>
-
-    <dimen name="qs_media_session_height_expanded">172dp</dimen>
-
     <!-- margin from keyguard status bar to clock. For split shade it should be
          keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp -->
     <dimen name="keyguard_clock_top_margin">8dp</dimen>
 
+    <!-- QS-->
+    <dimen name="qs_panel_padding_top">16dp</dimen>
+    <dimen name="qs_content_horizontal_padding">24dp</dimen>
+    <dimen name="qs_horizontal_margin">24dp</dimen>
+    <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2,
+         otherwise full space between two pages is qs_horizontal_margin*2, and that makes tiles page
+         not appear immediately after user swipes to the side -->
+    <dimen name="qs_tiles_page_horizontal_margin">12dp</dimen>
+    <dimen name="qs_media_session_height_expanded">172dp</dimen>
+
     <dimen name="split_shade_notifications_scrim_margin_bottom">16dp</dimen>
 
     <dimen name="notification_panel_margin_bottom">48dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 33d6f19..f45f106 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -26,6 +26,12 @@
     <dimen name="status_bar_header_height_keyguard">56dp</dimen>
 
     <dimen name="qs_media_session_height_expanded">251dp</dimen>
+    <dimen name="qs_content_horizontal_padding">40dp</dimen>
+    <dimen name="qs_horizontal_margin">40dp</dimen>
+    <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2,
+         otherwise full space between two pages is qs_horizontal_margin*2, and that makes tiles page
+         not appear immediately after user swipes to the side -->
+    <dimen name="qs_tiles_page_horizontal_margin">20dp</dimen>
 
     <dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen>
 
diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
index fc12d41..2abc9e3 100644
--- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
@@ -27,7 +27,7 @@
 
     <dimen name="qqs_layout_padding_bottom">40dp</dimen>
 
-    <dimen name="notification_panel_margin_horizontal">96dp</dimen>
+    <dimen name="notification_panel_margin_horizontal">80dp</dimen>
     <dimen name="notification_side_paddings">40dp</dimen>
     <dimen name="notification_section_divider_height">16dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f77430b..a014efb 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -478,6 +478,22 @@
     <dimen name="qqs_layout_margin_top">16dp</dimen>
     <dimen name="qqs_layout_padding_bottom">24dp</dimen>
 
+    <!-- Most of the time it should be the same as notification_side_paddings as it's vertically
+         aligned with notifications. The exception is split shade when this value becomes
+         independent  -->
+    <dimen name="qs_horizontal_margin">@dimen/notification_side_paddings</dimen>
+
+    <!-- Most of the time it should be the same as notification_shade_content_margin_horizontal as
+         it's vertically aligned with notifications. The exception is split shade when this value
+         becomes independent  -->
+    <dimen name="qs_content_horizontal_padding">@dimen/notification_shade_content_margin_horizontal</dimen>
+
+    <!-- Most of the time it should be the same as notification_side_paddings as it's vertically
+        aligned with notifications. That's not the case on large screen when we have either split
+        shade and QS is not above notifications or in portrait shade when notification scrim is no
+        longer full width and next page of tiles should be at the edge of the screen -->
+    <dimen name="qs_tiles_page_horizontal_margin">@dimen/notification_side_paddings</dimen>
+
     <dimen name="qs_customize_internal_side_paddings">8dp</dimen>
     <dimen name="qs_icon_size">20dp</dimen>
     <dimen name="qs_side_view_size">28dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8f412e3..2426f01 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1211,7 +1211,7 @@
     <string name="wallet_app_button_label">Show all</string>
     <!-- Label of the button underneath the card carousel prompting user unlock device. [CHAR LIMIT=NONE] -->
     <!-- Secondary label of the quick access wallet tile if no card. [CHAR LIMIT=NONE] -->
-    <string name="wallet_secondary_label_no_card">Add a card</string>
+    <string name="wallet_secondary_label_no_card">Tap to open</string>
     <!-- Secondary label of the quick access wallet tile if wallet is still updating. [CHAR LIMIT=NONE] -->
     <string name="wallet_secondary_label_updating">Updating</string>
     <!-- Secondary label of the quick access wallet tile if device locked. [CHAR LIMIT=NONE] -->
@@ -2229,11 +2229,7 @@
     <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string>
     <!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
     <string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
-    <!-- Text informing the user that their media is now playing on this device. [CHAR LIMIT=50] -->
-    <string name="media_transfer_playing_this_device" product="default">Playing on this phone</string>
-    <!-- Text informing the user that their media is now playing on this tablet device. [CHAR LIMIT=50] -->
-    <string name="media_transfer_playing_this_device" product="tablet">Playing on this tablet</string>
-    <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] -->
+    <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] -->
     <string name="media_transfer_failed">Something went wrong. Try again.</string>
 
     <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
@@ -2486,6 +2482,8 @@
     <string name="clipboard_edit_image_description">Edit copied image</string>
     <!-- Label for button to send copied content to a nearby device [CHAR LIMIT=NONE] -->
     <string name="clipboard_send_nearby_description">Send to nearby device</string>
+    <!-- Text informing user that copied content is hidden [CHAR LIMIT=NONE] -->
+    <string name="clipboard_text_hidden">Tap to view</string>
 
     <!-- Generic "add" string [CHAR LIMIT=NONE] -->
     <string name="add">Add</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index c93c065..f9e73ec 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1111,8 +1111,9 @@
     </style>
 
     <style name="FgsManagerAppDuration">
-        <item name="android:fontFamily">?android:attr/textAppearanceSmall</item>
         <item name="android:textDirection">locale</item>
+        <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
     <style name="BroadcastDialog">
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 6a68c70..98ac640 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -28,7 +28,6 @@
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -169,7 +168,7 @@
         private final EmergencyButtonController.Factory mEmergencyButtonControllerFactory;
         private final FalsingCollector mFalsingCollector;
         private final DevicePostureController mDevicePostureController;
-        private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+        private final KeyguardViewController mKeyguardViewController;
 
         @Inject
         public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -181,7 +180,7 @@
                 TelephonyManager telephonyManager, FalsingCollector falsingCollector,
                 EmergencyButtonController.Factory emergencyButtonControllerFactory,
                 DevicePostureController devicePostureController,
-                StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+                KeyguardViewController keyguardViewController) {
             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
             mLockPatternUtils = lockPatternUtils;
             mLatencyTracker = latencyTracker;
@@ -194,7 +193,7 @@
             mEmergencyButtonControllerFactory = emergencyButtonControllerFactory;
             mFalsingCollector = falsingCollector;
             mDevicePostureController = devicePostureController;
-            mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+            mKeyguardViewController = keyguardViewController;
         }
 
         /** Create a new {@link KeyguardInputViewController}. */
@@ -215,7 +214,7 @@
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
                         keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
                         mInputMethodManager, emergencyButtonController, mMainExecutor, mResources,
-                        mFalsingCollector, mStatusBarKeyguardViewManager);
+                        mFalsingCollector, mKeyguardViewController);
 
             } else if (keyguardInputView instanceof KeyguardPINView) {
                 return new KeyguardPinViewController((KeyguardPINView) keyguardInputView,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 1903526..8f44e97 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -43,7 +43,6 @@
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.util.List;
@@ -56,7 +55,7 @@
     private final KeyguardSecurityCallback mKeyguardSecurityCallback;
     private final InputMethodManager mInputMethodManager;
     private final DelayableExecutor mMainExecutor;
-    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final KeyguardViewController mKeyguardViewController;
     private final boolean mShowImeAtScreenOn;
     private EditText mPasswordEntry;
     private ImageView mSwitchImeButton;
@@ -119,14 +118,14 @@
             @Main DelayableExecutor mainExecutor,
             @Main Resources resources,
             FalsingCollector falsingCollector,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+            KeyguardViewController keyguardViewController) {
         super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
                 messageAreaControllerFactory, latencyTracker, falsingCollector,
                 emergencyButtonController);
         mKeyguardSecurityCallback = keyguardSecurityCallback;
         mInputMethodManager = inputMethodManager;
         mMainExecutor = mainExecutor;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mKeyguardViewController = keyguardViewController;
         mShowImeAtScreenOn = resources.getBoolean(R.bool.kg_show_ime_at_screen_on);
         mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
         mSwitchImeButton = mView.findViewById(R.id.switch_ime_button);
@@ -209,7 +208,7 @@
     }
 
     private void showInput() {
-        if (!mStatusBarKeyguardViewManager.isBouncerShowing()) {
+        if (!mKeyguardViewController.isBouncerShowing()) {
             return;
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 41f9240..39c3949 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -282,7 +282,7 @@
         super.reloadColors();
         mMessageAreaController.reloadColors();
         int textColor = Utils.getColorAttr(mLockPatternView.getContext(),
-                android.R.attr.textColorPrimary).getDefaultColor();
+                android.R.attr.textColorSecondary).getDefaultColor();
         int errorColor = Utils.getColorError(mLockPatternView.getContext()).getDefaultColor();
         mLockPatternView.setColors(textColor, textColor, errorColor);
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
index caf7ee4..c91c899 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
@@ -30,7 +30,6 @@
 
 import androidx.annotation.StyleRes;
 
-import com.android.settingslib.Utils;
 import com.android.systemui.animation.Interpolators;
 
 /**
@@ -42,24 +41,29 @@
     private ValueAnimator mContractAnimator;
     private AnimatorSet mContractAnimatorSet;
     private GradientDrawable mBackground;
-    private int mNormalColor;
-    private int mHighlightColor;
-    private int mStyle;
+    private Drawable mImageButton;
     private TextView mDigitTextView;
+    private int mNormalBackgroundColor;
+    private int mPressedBackgroundColor;
+    private int mTextColorPrimary;
+    private int mTextColorPressed;
+    private int mStyle;
     private static final int EXPAND_ANIMATION_MS = 100;
     private static final int EXPAND_COLOR_ANIMATION_MS = 50;
     private static final int CONTRACT_ANIMATION_DELAY_MS = 33;
     private static final int CONTRACT_ANIMATION_MS = 417;
 
-    NumPadAnimator(Context context, final Drawable drawable, @StyleRes int style) {
-        this(context, drawable, style, null);
+    NumPadAnimator(Context context, final Drawable drawable,
+            @StyleRes int style, Drawable buttonImage) {
+        this(context, drawable, style, null, buttonImage);
     }
 
     NumPadAnimator(Context context, final Drawable drawable, @StyleRes int style,
-            @Nullable TextView digitTextView) {
+            @Nullable TextView digitTextView, @Nullable Drawable buttonImage) {
         mStyle = style;
         mBackground = (GradientDrawable) drawable;
         mDigitTextView = digitTextView;
+        mImageButton = buttonImage;
 
         reloadColors(context);
     }
@@ -88,26 +92,28 @@
      * Reload colors from resources.
      **/
     void reloadColors(Context context) {
-        int[] customAttrs = {android.R.attr.colorControlNormal,
-                android.R.attr.colorControlHighlight};
+        boolean isNumPadKey = mImageButton == null;
 
+        int[] customAttrs = {android.R.attr.colorControlNormal};
         ContextThemeWrapper ctw = new ContextThemeWrapper(context, mStyle);
         TypedArray a = ctw.obtainStyledAttributes(customAttrs);
-        mNormalColor = getPrivateAttrColorIfUnset(ctw, a, 0, 0,
+        mNormalBackgroundColor = getPrivateAttrColorIfUnset(ctw, a, 0, 0,
                 com.android.internal.R.attr.colorSurface);
-        mHighlightColor = a.getColor(1, 0);
         a.recycle();
+        mBackground.setColor(mNormalBackgroundColor);
 
-        mBackground.setColor(mNormalColor);
-        createAnimators(context);
+        mPressedBackgroundColor = context.getColor(android.R.color.system_accent1_200);
+        mTextColorPrimary = isNumPadKey
+                ? com.android.settingslib.Utils
+                .getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
+                : com.android.settingslib.Utils
+                        .getColorAttrDefaultColor(context, android.R.attr.textColorPrimaryInverse);
+        mTextColorPressed = com.android.settingslib.Utils
+                .getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorOnAccent);
+        createAnimators();
     }
 
-    private void createAnimators(Context context) {
-        int textColorPrimary = Utils.getColorAttrDefaultColor(context,
-                android.R.attr.textColorPrimary);
-        int textColorPrimaryInverse = Utils.getColorAttrDefaultColor(context,
-                android.R.attr.textColorPrimaryInverse);
-
+    private void createAnimators() {
         // Actual values will be updated later, usually during an onLayout() call
         mExpandAnimator = ValueAnimator.ofFloat(0f, 1f);
         mExpandAnimator.setDuration(EXPAND_ANIMATION_MS);
@@ -116,7 +122,7 @@
                 anim -> mBackground.setCornerRadius((float) anim.getAnimatedValue()));
 
         ValueAnimator expandBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(),
-                mNormalColor, mHighlightColor);
+                mNormalBackgroundColor, mPressedBackgroundColor);
         expandBackgroundColorAnimator.setDuration(EXPAND_COLOR_ANIMATION_MS);
         expandBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
         expandBackgroundColorAnimator.addUpdateListener(
@@ -124,13 +130,16 @@
 
         ValueAnimator expandTextColorAnimator =
                 ValueAnimator.ofObject(new ArgbEvaluator(),
-                textColorPrimary, textColorPrimaryInverse);
+                mTextColorPrimary, mTextColorPressed);
         expandTextColorAnimator.setInterpolator(Interpolators.LINEAR);
         expandTextColorAnimator.setDuration(EXPAND_COLOR_ANIMATION_MS);
         expandTextColorAnimator.addUpdateListener(valueAnimator -> {
             if (mDigitTextView != null) {
                 mDigitTextView.setTextColor((int) valueAnimator.getAnimatedValue());
             }
+            if (mImageButton != null) {
+                mImageButton.setTint((int) valueAnimator.getAnimatedValue());
+            }
         });
 
         mExpandAnimatorSet = new AnimatorSet();
@@ -144,7 +153,7 @@
         mContractAnimator.addUpdateListener(
                 anim -> mBackground.setCornerRadius((float) anim.getAnimatedValue()));
         ValueAnimator contractBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(),
-                mHighlightColor, mNormalColor);
+                mPressedBackgroundColor, mNormalBackgroundColor);
         contractBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
         contractBackgroundColorAnimator.setStartDelay(CONTRACT_ANIMATION_DELAY_MS);
         contractBackgroundColorAnimator.setDuration(CONTRACT_ANIMATION_MS);
@@ -152,8 +161,8 @@
                 animator -> mBackground.setColor((int) animator.getAnimatedValue()));
 
         ValueAnimator contractTextColorAnimator =
-                ValueAnimator.ofObject(new ArgbEvaluator(), textColorPrimaryInverse,
-                textColorPrimary);
+                ValueAnimator.ofObject(new ArgbEvaluator(), mTextColorPressed,
+                mTextColorPrimary);
         contractTextColorAnimator.setInterpolator(Interpolators.LINEAR);
         contractTextColorAnimator.setStartDelay(CONTRACT_ANIMATION_DELAY_MS);
         contractTextColorAnimator.setDuration(CONTRACT_ANIMATION_MS);
@@ -161,6 +170,9 @@
             if (mDigitTextView != null) {
                 mDigitTextView.setTextColor((int) valueAnimator.getAnimatedValue());
             }
+            if (mImageButton != null) {
+                mImageButton.setTint((int) valueAnimator.getAnimatedValue());
+            }
         });
 
         mContractAnimatorSet = new AnimatorSet();
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
index 7b98f27..8099f75 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
@@ -42,7 +42,7 @@
         Drawable background = getBackground();
         if (background instanceof GradientDrawable) {
             mAnimator = new NumPadAnimator(context, background.mutate(),
-                    attrs.getStyleAttribute());
+                    attrs.getStyleAttribute(), getDrawable());
         } else {
             mAnimator = null;
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
index f771c97..4aed251 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
@@ -134,7 +134,7 @@
         Drawable background = getBackground();
         if (background instanceof GradientDrawable) {
             mAnimator = new NumPadAnimator(context, background.mutate(),
-                    R.style.NumPadKey, mDigitText);
+                    R.style.NumPadKey, mDigitText, null);
         } else {
             mAnimator = null;
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
index 6e06130..334bb1e 100644
--- a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
@@ -76,12 +76,6 @@
     void playTrustedSound();
 
     /**
-     * When the bouncer is shown or hides
-     * @param shown
-     */
-    void onBouncerVisiblityChanged(boolean shown);
-
-    /**
      * @return true if the screen is on
      */
     boolean isScreenOn();
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 8d65098..cbce854 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -899,8 +899,8 @@
         final DisplayInfo displayInfo = new DisplayInfo();
         mContext.getDisplay().getDisplayInfo(displayInfo);
         return DisplayUtils.getPhysicalPixelDisplaySizeRatio(
-                stableDisplaySize.x, stableDisplaySize.y, displayInfo.logicalWidth,
-                displayInfo.logicalHeight);
+                stableDisplaySize.x, stableDisplaySize.y, displayInfo.getNaturalWidth(),
+                displayInfo.getNaturalHeight());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index d59ad92..9324893 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -278,6 +278,7 @@
                 }
             });
             mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation);
+            mUdfpsController.setHalControlsIllumination(mUdfpsProps.get(0).halControlsIllumination);
             mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect();
             updateUdfpsLocation();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 76c1dbc..d9aa1ba 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -80,6 +80,7 @@
 
     @VisibleForTesting
     internal var startLightRevealScrimOnKeyguardFadingAway = false
+    var lightRevealScrimAnimator: ValueAnimator? = null
     var fingerprintSensorLocation: PointF? = null
     private var faceSensorLocation: PointF? = null
     private var circleReveal: LightRevealEffect? = null
@@ -163,7 +164,8 @@
         if (keyguardStateController.isKeyguardFadingAway) {
             val lightRevealScrim = centralSurfaces.lightRevealScrim
             if (startLightRevealScrimOnKeyguardFadingAway && lightRevealScrim != null) {
-                ValueAnimator.ofFloat(.1f, 1f).apply {
+                lightRevealScrimAnimator?.cancel()
+                lightRevealScrimAnimator = ValueAnimator.ofFloat(.1f, 1f).apply {
                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN
                     duration = RIPPLE_ANIMATION_DURATION
                     startDelay = keyguardStateController.keyguardFadingAwayDelay
@@ -183,6 +185,8 @@
                             if (lightRevealScrim.revealEffect == circleReveal) {
                                 lightRevealScrim.revealEffect = LiftReveal
                             }
+
+                            lightRevealScrimAnimator = null
                         }
                     })
                     start()
@@ -192,6 +196,13 @@
         }
     }
 
+    /**
+     * Whether we're animating the light reveal scrim from a call to [onKeyguardFadingAwayChanged].
+     */
+    fun isAnimatingLightRevealScrim(): Boolean {
+        return lightRevealScrimAnimator?.isRunning ?: false
+    }
+
     override fun onStartedGoingToSleep() {
         // reset the light reveal start in case we were pending an unlock
         startLightRevealScrimOnKeyguardFadingAway = false
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 432d293..a35f042 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -131,6 +131,7 @@
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
     @VisibleForTesting int mSensorId;
+    private boolean mHalControlsIllumination;
     @VisibleForTesting @NonNull UdfpsOverlayParams mOverlayParams = new UdfpsOverlayParams();
     // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this.
     @Nullable private Runnable mAuthControllerUpdateUdfpsLocation;
@@ -201,9 +202,10 @@
                             mKeyguardUpdateMonitor, mDialogManager, mDumpManager,
                             mLockscreenShadeTransitionController, mConfigurationController,
                             mSystemClock, mKeyguardStateController,
-                            mUnlockedScreenOffAnimationController, mHbmProvider, requestId, reason,
-                            callback, (view, event, fromUdfpsView) -> onTouch(requestId, event,
-                            fromUdfpsView), mActivityLaunchAnimator)));
+                            mUnlockedScreenOffAnimationController, mHalControlsIllumination,
+                            mHbmProvider, requestId, reason, callback,
+                            (view, event, fromUdfpsView) -> onTouch(requestId, event,
+                                    fromUdfpsView), mActivityLaunchAnimator)));
         }
 
         @Override
@@ -310,6 +312,11 @@
         mAuthControllerUpdateUdfpsLocation = r;
     }
 
+    // TODO(b/229290039): UDFPS controller should manage its properties on its own. Remove this.
+    public void setHalControlsIllumination(boolean value) {
+        mHalControlsIllumination = value;
+    }
+
     /**
      * Calculate the pointer speed given a velocity tracker and the pointer id.
      * This assumes that the velocity tracker has already been passed all relevant motion events.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 9c8aee4..2d51c97 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -77,6 +77,7 @@
     private val systemClock: SystemClock,
     private val keyguardStateController: KeyguardStateController,
     private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+    private val halControlsIllumination: Boolean,
     private var hbmProvider: UdfpsHbmProvider,
     val requestId: Long,
     @ShowReason val requestReason: Int,
@@ -137,6 +138,7 @@
                     R.layout.udfps_view, null, false
                 ) as UdfpsView).apply {
                     overlayParams = params
+                    halControlsIllumination = this@UdfpsControllerOverlay.halControlsIllumination
                     setHbmProvider(hbmProvider)
                     val animation = inflateUdfpsAnimation(this, controller)
                     if (animation != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHbmProvider.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHbmProvider.java
index 38c937f..f26dd5f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHbmProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHbmProvider.java
@@ -34,8 +34,11 @@
      * invoked from the UI thread.
      *
      * @param onHbmEnabled A runnable that will be executed once HBM is enabled.
+     *
+     * TODO(b/231335067): enableHbm with halControlsIllumination=true shouldn't make sense.
+     *     This only makes sense now because vendor code may rely on the side effects of enableHbm.
      */
-    void enableHbm(@Nullable Runnable onHbmEnabled);
+    void enableHbm(boolean halControlsIllumination, @Nullable Runnable onHbmEnabled);
 
     /**
      * UdfpsView will call this to disable HBM when illumination is no longer needed.
@@ -46,8 +49,6 @@
      * The call must be made from the UI thread. The callback, if provided, will also be invoked
      * from the UI thread.
      *
-     *
-     *
      * @param onHbmDisabled A runnable that will be executed once HBM is disabled.
      */
     void disableHbm(@Nullable Runnable onHbmDisabled);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
index 2aa345a..245c225 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
@@ -63,9 +63,12 @@
     /** View controller (can be different for enrollment, BiometricPrompt, Keyguard, etc.). */
     var animationViewController: UdfpsAnimationViewController<*>? = null
 
-    /** Parameters that affect the position and size of the overlay. Visible for testing. */
+    /** Parameters that affect the position and size of the overlay. */
     var overlayParams = UdfpsOverlayParams()
 
+    /** Whether the HAL is responsible for enabling and disabling of LHBM. */
+    var halControlsIllumination: Boolean = true
+
     /** Debug message. */
     var debugMessage: String? = null
         set(value) {
@@ -154,11 +157,17 @@
     }
 
     private fun doIlluminate(onIlluminatedRunnable: Runnable?) {
-        hbmProvider?.enableHbm() {
+        // TODO(b/231335067): enableHbm with halControlsIllumination=true shouldn't make sense.
+        // This only makes sense now because vendor code may rely on the side effects of enableHbm.
+        hbmProvider?.enableHbm(halControlsIllumination) {
             if (onIlluminatedRunnable != null) {
-                // No framework API can reliably tell when a frame reaches the panel. A timeout
-                // is the safest solution.
-                postDelayed(onIlluminatedRunnable, onIlluminatedDelayMs)
+                if (halControlsIllumination) {
+                    onIlluminatedRunnable.run()
+                } else {
+                    // No framework API can reliably tell when a frame reaches the panel. A timeout
+                    // is the safest solution.
+                    postDelayed(onIlluminatedRunnable, onIlluminatedDelayMs)
+                }
             } else {
                 Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null")
             }
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 726d00c..ee8363f 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -38,6 +38,7 @@
 import android.app.RemoteAction;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -128,6 +129,8 @@
     private final View mClipboardPreview;
     private final ImageView mImagePreview;
     private final TextView mTextPreview;
+    private final TextView mHiddenTextPreview;
+    private final TextView mHiddenImagePreview;
     private final View mPreviewBorder;
     private final OverlayActionChip mEditChip;
     private final OverlayActionChip mRemoteCopyChip;
@@ -186,6 +189,8 @@
         mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
         mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
         mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
+        mHiddenTextPreview = requireNonNull(mView.findViewById(R.id.hidden_text_preview));
+        mHiddenImagePreview = requireNonNull(mView.findViewById(R.id.hidden_image_preview));
         mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
         mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
         mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
@@ -270,21 +275,32 @@
             mExitAnimator.cancel();
         }
         reset();
+
+        boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
+                && clipData.getDescription().getExtras()
+                .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
         if (clipData == null || clipData.getItemCount() == 0) {
-            showTextPreview(mContext.getResources().getString(
-                    R.string.clipboard_overlay_text_copied));
+            showTextPreview(
+                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+                    mTextPreview);
         } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
             ClipData.Item item = clipData.getItemAt(0);
             if (item.getTextLinks() != null) {
                 AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource));
             }
-            showEditableText(item.getText());
+            if (isSensitive) {
+                showEditableText(
+                        mContext.getResources().getString(R.string.clipboard_text_hidden), true);
+            } else {
+                showEditableText(item.getText(), false);
+            }
         } else if (clipData.getItemAt(0).getUri() != null) {
             // How to handle non-image URIs?
-            showEditableImage(clipData.getItemAt(0).getUri());
+            showEditableImage(clipData.getItemAt(0).getUri(), isSensitive);
         } else {
             showTextPreview(
-                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied));
+                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+                    mTextPreview);
         }
         Intent remoteCopyIntent = getRemoteCopyIntent(clipData);
         // Only show remote copy if it's available.
@@ -406,15 +422,23 @@
         animateOut();
     }
 
-    private void showTextPreview(CharSequence text) {
-        mTextPreview.setVisibility(View.VISIBLE);
+    private void showSinglePreview(View v) {
+        mTextPreview.setVisibility(View.GONE);
         mImagePreview.setVisibility(View.GONE);
-        mTextPreview.setText(text.subSequence(0, Math.min(500, text.length())));
+        mHiddenTextPreview.setVisibility(View.GONE);
+        mHiddenImagePreview.setVisibility(View.GONE);
+        v.setVisibility(View.VISIBLE);
+    }
+
+    private void showTextPreview(CharSequence text, TextView textView) {
+        showSinglePreview(textView);
+        textView.setText(text.subSequence(0, Math.min(500, text.length())));
         mEditChip.setVisibility(View.GONE);
     }
 
-    private void showEditableText(CharSequence text) {
-        showTextPreview(text);
+    private void showEditableText(CharSequence text, boolean hidden) {
+        TextView textView = hidden ? mHiddenTextPreview : mTextPreview;
+        showTextPreview(text, textView);
         mEditChip.setVisibility(View.VISIBLE);
         mActionContainerBackground.setVisibility(View.VISIBLE);
         mEditChip.setAlpha(1f);
@@ -422,32 +446,36 @@
                 mContext.getString(R.string.clipboard_edit_text_description));
         View.OnClickListener listener = v -> editText();
         mEditChip.setOnClickListener(listener);
-        mTextPreview.setOnClickListener(listener);
+        textView.setOnClickListener(listener);
     }
 
-    private void showEditableImage(Uri uri) {
-        ContentResolver resolver = mContext.getContentResolver();
-        try {
-            int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
-            // The width of the view is capped, height maintains aspect ratio, so allow it to be
-            // taller if needed.
-            Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
-            mImagePreview.setImageBitmap(thumbnail);
-        } catch (IOException e) {
-            Log.e(TAG, "Thumbnail loading failed", e);
-            showTextPreview(
-                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied));
-            return;
-        }
-        mTextPreview.setVisibility(View.GONE);
-        mImagePreview.setVisibility(View.VISIBLE);
+    private void showEditableImage(Uri uri, boolean isSensitive) {
         mEditChip.setAlpha(1f);
         mActionContainerBackground.setVisibility(View.VISIBLE);
         View.OnClickListener listener = v -> editImage(uri);
+        if (isSensitive) {
+            showSinglePreview(mHiddenImagePreview);
+            mHiddenImagePreview.setOnClickListener(listener);
+        } else {
+            showSinglePreview(mImagePreview);
+            ContentResolver resolver = mContext.getContentResolver();
+            try {
+                int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
+                // The width of the view is capped, height maintains aspect ratio, so allow it to be
+                // taller if needed.
+                Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
+                mImagePreview.setImageBitmap(thumbnail);
+            } catch (IOException e) {
+                Log.e(TAG, "Thumbnail loading failed", e);
+                showTextPreview(
+                        mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+                        mTextPreview);
+            }
+            mImagePreview.setOnClickListener(listener);
+        }
         mEditChip.setOnClickListener(listener);
         mEditChip.setContentDescription(
                 mContext.getString(R.string.clipboard_edit_image_description));
-        mImagePreview.setOnClickListener(listener);
     }
 
     private Intent getRemoteCopyIntent(ClipData clipData) {
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java
index 0d89879..b54b832 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java
@@ -20,10 +20,12 @@
 
 import android.app.Activity;
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.ClipboardManager;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.util.Log;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
@@ -41,6 +43,7 @@
     private EditText mEditText;
     private ClipboardManager mClipboardManager;
     private TextView mAttribution;
+    private boolean mSensitive;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -72,6 +75,9 @@
         }
         mEditText.setText(clip.getItemAt(0).getText());
         mEditText.requestFocus();
+        mSensitive = clip.getDescription().getExtras() != null
+                && clip.getDescription().getExtras()
+                .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
         mClipboardManager.addPrimaryClipChangedListener(this);
     }
 
@@ -88,6 +94,9 @@
 
     private void saveToClipboard() {
         ClipData clip = ClipData.newPlainText("text", mEditText.getText());
+        PersistableBundle extras = new PersistableBundle();
+        extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, mSensitive);
+        clip.getDescription().setExtras(extras);
         mClipboardManager.setPrimaryClip(clip);
         hideImeAndFinish();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index f9115b2..3eb58bb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -343,7 +343,7 @@
         info.className = Switch::class.java.name
     }
 
-    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+    override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
         if (super.performAccessibilityAction(host, action, args)) {
             return true
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index fb09132..14a7e3c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController
 import com.android.systemui.shared.system.smartspace.SmartspaceState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import dagger.Lazy
@@ -140,7 +141,8 @@
     keyguardViewMediator: Lazy<KeyguardViewMediator>,
     private val keyguardViewController: KeyguardViewController,
     private val featureFlags: FeatureFlags,
-    private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>
+    private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
+    private val statusBarStateController: SysuiStatusBarStateController
 ) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() {
 
     interface KeyguardUnlockAnimationListener {
@@ -372,7 +374,8 @@
      * changed.
      */
     override fun onKeyguardGoingAwayChanged() {
-        if (keyguardStateController.isKeyguardGoingAway) {
+        if (keyguardStateController.isKeyguardGoingAway
+            && !statusBarStateController.leaveOpenOnKeyguardHide()) {
             prepareForInWindowLauncherAnimations()
         }
     }
@@ -813,6 +816,11 @@
             return false
         }
 
+        // The smartspace is not visible if the bouncer is showing, so don't shared element it.
+        if (keyguardStateController.isBouncerShowing) {
+            return false
+        }
+
         // We started to swipe to dismiss, but now we're doing a fling animation to complete the
         // dismiss. In this case, the smartspace swiped away with the rest of the keyguard, so don't
         // do the shared element transition.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 10ea1e0..4d59f1a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -779,16 +779,6 @@
         }
 
         @Override
-        public void onBouncerVisiblityChanged(boolean shown) {
-            synchronized (KeyguardViewMediator.this) {
-                if (shown) {
-                    mPendingPinLock = false;
-                }
-                adjustStatusBarLocked(shown, false);
-            }
-        }
-
-        @Override
         public void playTrustedSound() {
             KeyguardViewMediator.this.playTrustedSound();
         }
@@ -921,12 +911,12 @@
                         RemoteAnimationTarget[] wallpapers,
                         RemoteAnimationTarget[] nonApps,
                         IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
+                    setOccluded(false /* isOccluded */, true /* animate */);
+
                     if (apps == null || apps.length == 0 || apps[0] == null) {
                         Log.d(TAG, "No apps provided to unocclude runner; "
                                 + "skipping animation and unoccluding.");
-
                         finishedCallback.onAnimationFinished();
-                        setOccluded(false /* isOccluded */, true /* animate */);
                         return;
                     }
 
@@ -971,7 +961,6 @@
                             @Override
                             public void onAnimationEnd(Animator animation) {
                                 try {
-                                    setOccluded(false /* isOccluded */, true /* animate */);
                                     finishedCallback.onAnimationFinished();
                                     mUnoccludeAnimator = null;
                                 } catch (RemoteException e) {
@@ -989,6 +978,19 @@
     private DozeParameters mDozeParameters;
 
     private final KeyguardStateController mKeyguardStateController;
+    private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
+            new KeyguardStateController.Callback() {
+        @Override
+        public void onBouncerShowingChanged() {
+            synchronized (KeyguardViewMediator.this) {
+                if (mKeyguardStateController.isBouncerShowing()) {
+                    mPendingPinLock = false;
+                }
+                adjustStatusBarLocked(mKeyguardStateController.isBouncerShowing(), false);
+            }
+        }
+    };
+
     private final Lazy<KeyguardUnlockAnimationController> mKeyguardUnlockAnimationControllerLazy;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private boolean mWallpaperSupportsAmbientMode;
@@ -1059,6 +1061,7 @@
         statusBarStateController.addCallback(this);
 
         mKeyguardStateController = keyguardStateController;
+        keyguardStateController.addCallback(mKeyguardStateControllerCallback);
         mKeyguardUnlockAnimationControllerLazy = keyguardUnlockAnimationControllerLazy;
         mScreenOffAnimationController = screenOffAnimationController;
         mInteractionJankMonitor = interactionJankMonitor;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 5bb6557..8dcca3d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -449,7 +449,7 @@
     abstract int getStopButtonVisibility();
 
     public CharSequence getStopButtonText() {
-        return mContext.getText(R.string.keyboard_key_media_stop);
+        return mContext.getText(R.string.media_output_dialog_button_stop_casting);
     }
 
     public void onStopButtonClick() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index e7f97d2..58b6ad3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -55,8 +55,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.mediarouter.media.MediaRouter;
-import androidx.mediarouter.media.MediaRouterParams;
 
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.Utils;
@@ -215,9 +213,8 @@
     }
 
     boolean shouldShowLaunchSection() {
-        MediaRouterParams routerParams = MediaRouter.getInstance(mContext).getRouterParams();
-        Log.d(TAG, "try to get routerParams: " + routerParams);
-        return routerParams != null && !routerParams.isMediaTransferReceiverEnabled();
+        // TODO(b/231398073): Implements this when available.
+        return false;
     }
 
     void setRefreshing(boolean refreshing) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 9248433..026a305 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -104,7 +104,7 @@
 
     @Override
     public CharSequence getStopButtonText() {
-        int resId = R.string.keyboard_key_media_stop;
+        int resId = R.string.media_output_dialog_button_stop_casting;
         if (isBroadcastSupported() && mMediaOutputController.isPlaying()
                 && !mMediaOutputController.isBluetoothLeBroadcastEnabled()) {
             resId = R.string.media_output_broadcast;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index c7cd48f..3bb4e64 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -125,6 +125,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.rotation.RotationButton;
 import com.android.systemui.shared.rotation.RotationButtonController;
@@ -203,6 +204,7 @@
     private final UiEventLogger mUiEventLogger;
     private final NavBarHelper mNavBarHelper;
     private final NotificationShadeDepthController mNotificationShadeDepthController;
+    private final UserContextProvider mUserContextProvider;
     private NavigationBarFrame mFrame;
 
     private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
@@ -514,7 +516,8 @@
             DeadZone deadZone,
             DeviceConfigProxy deviceConfigProxy,
             NavigationBarTransitions navigationBarTransitions,
-            Optional<BackAnimation> backAnimation) {
+            Optional<BackAnimation> backAnimation,
+            UserContextProvider userContextProvider) {
         super(navigationBarView);
         mFrame = navigationBarFrame;
         mContext = context;
@@ -550,6 +553,7 @@
         mAutoHideControllerFactory = autoHideControllerFactory;
         mTelecomManagerOptional = telecomManagerOptional;
         mInputMethodManager = inputMethodManager;
+        mUserContextProvider = userContextProvider;
 
         mNavBarMode = mNavigationModeController.addListener(mModeChangedListener);
     }
@@ -1512,35 +1516,36 @@
         int insetsHeight = -1;
         int gravity = Gravity.BOTTOM;
         boolean navBarCanMove = true;
+        final Context userContext = mUserContextProvider.createCurrentUserContext(mContext);
         if (mWindowManager != null && mWindowManager.getCurrentWindowMetrics() != null) {
             Rect displaySize = mWindowManager.getCurrentWindowMetrics().getBounds();
             navBarCanMove = displaySize.width() != displaySize.height()
-                    && mContext.getResources().getBoolean(
+                    && userContext.getResources().getBoolean(
                     com.android.internal.R.bool.config_navBarCanMove);
         }
         if (!navBarCanMove) {
-            height = mContext.getResources().getDimensionPixelSize(
+            height = userContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.navigation_bar_frame_height);
-            insetsHeight = mContext.getResources().getDimensionPixelSize(
+            insetsHeight = userContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.navigation_bar_height);
         } else {
             switch (rotation) {
                 case ROTATION_UNDEFINED:
                 case Surface.ROTATION_0:
                 case Surface.ROTATION_180:
-                    height = mContext.getResources().getDimensionPixelSize(
+                    height = userContext.getResources().getDimensionPixelSize(
                             com.android.internal.R.dimen.navigation_bar_frame_height);
-                    insetsHeight = mContext.getResources().getDimensionPixelSize(
+                    insetsHeight = userContext.getResources().getDimensionPixelSize(
                             com.android.internal.R.dimen.navigation_bar_height);
                     break;
                 case Surface.ROTATION_90:
                     gravity = Gravity.RIGHT;
-                    width = mContext.getResources().getDimensionPixelSize(
+                    width = userContext.getResources().getDimensionPixelSize(
                             com.android.internal.R.dimen.navigation_bar_width);
                     break;
                 case Surface.ROTATION_270:
                     gravity = Gravity.LEFT;
-                    width = mContext.getResources().getDimensionPixelSize(
+                    width = userContext.getResources().getDimensionPixelSize(
                             com.android.internal.R.dimen.navigation_bar_width);
                     break;
             }
@@ -1565,12 +1570,12 @@
             lp.providedInternalInsets[ITYPE_NAVIGATION_BAR] = null;
         }
         lp.token = new Binder();
-        lp.accessibilityTitle = mContext.getString(R.string.nav_bar);
+        lp.accessibilityTitle = userContext.getString(R.string.nav_bar);
         lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC
                 | WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
         lp.windowAnimations = 0;
-        lp.setTitle("NavigationBar" + mContext.getDisplayId());
+        lp.setTitle("NavigationBar" + userContext.getDisplayId());
         lp.setFitInsetsTypes(0 /* types */);
         lp.setTrustedOverlay();
         return lp;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 3eb4b10..3e8cdf3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -420,7 +420,7 @@
                 PowerExemptionManager.REASON_SYSTEM_UID,
                 PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY
 
-                PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE,
+                PowerExemptionManager.REASON_SYSTEM_ALLOW_LISTED,
                 PowerExemptionManager.REASON_DEVICE_OWNER,
                 PowerExemptionManager.REASON_DISALLOW_APPS_CONTROL,
                 PowerExemptionManager.REASON_DPO_PROTECTED_APP,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 7d3df6e..5d2060d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -47,9 +47,10 @@
     private QSCustomizer mQSCustomizer;
     private NonInterceptingScrollView mQSPanelContainer;
 
-    private int mSideMargins;
+    private int mHorizontalMargins;
+    private int mTilesPageMargin;
     private boolean mQsDisabled;
-    private int mContentPadding = -1;
+    private int mContentHorizontalPadding = -1;
     private boolean mClippingEnabled;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
@@ -145,12 +146,17 @@
                 mQSPanelContainer.getPaddingEnd(),
                 bottomPadding);
 
-        int sideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
-        int padding = getResources().getDimensionPixelSize(
-                R.dimen.notification_shade_content_margin_horizontal);
-        boolean marginsChanged = padding != mContentPadding || sideMargins != mSideMargins;
-        mContentPadding = padding;
-        mSideMargins = sideMargins;
+        int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
+        int horizontalPadding = getResources().getDimensionPixelSize(
+                R.dimen.qs_content_horizontal_padding);
+        int tilesPageMargin = getResources().getDimensionPixelSize(
+                R.dimen.qs_tiles_page_horizontal_margin);
+        boolean marginsChanged = horizontalPadding != mContentHorizontalPadding
+                || horizontalMargins != mHorizontalMargins
+                || tilesPageMargin != mTilesPageMargin;
+        mContentHorizontalPadding = horizontalPadding;
+        mHorizontalMargins = horizontalMargins;
+        mTilesPageMargin = tilesPageMargin;
         if (marginsChanged) {
             updatePaddingsAndMargins(qsPanelController, quickStatusBarHeaderController);
         }
@@ -198,22 +204,22 @@
                 // Only padding for FooterActionsView, no margin. That way, the background goes
                 // all the way to the edge.
                 LayoutParams lp = (LayoutParams) view.getLayoutParams();
-                lp.rightMargin = mSideMargins;
-                lp.leftMargin = mSideMargins;
+                lp.rightMargin = mHorizontalMargins;
+                lp.leftMargin = mHorizontalMargins;
             }
             if (view == mQSPanelContainer) {
                 // QS panel lays out some of its content full width
-                qsPanelController.setContentMargins(mContentPadding, mContentPadding);
-                // Set it as double the side margin (to simulate end margin of current page +
-                // start margin of next page).
-                qsPanelController.setPageMargin(mSideMargins);
+                qsPanelController.setContentMargins(mContentHorizontalPadding,
+                        mContentHorizontalPadding);
+                qsPanelController.setPageMargin(mTilesPageMargin);
             } else if (view == mHeader) {
-                quickStatusBarHeaderController.setContentMargins(mContentPadding, mContentPadding);
+                quickStatusBarHeaderController.setContentMargins(mContentHorizontalPadding,
+                        mContentHorizontalPadding);
             } else {
                 view.setPaddingRelative(
-                        mContentPadding,
+                        mContentHorizontalPadding,
                         view.getPaddingTop(),
-                        mContentPadding,
+                        mContentHorizontalPadding,
                         view.getPaddingBottom());
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index 772e9fa..248c78e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -147,16 +147,9 @@
         if (mController.getWalletClient().isWalletServiceAvailable()
                 && mController.getWalletClient().isWalletFeatureAvailable()) {
             if (mSelectedCard != null) {
-                if (isDeviceLocked) {
-                    state.state = Tile.STATE_INACTIVE;
-                    state.secondaryLabel =
-                            mContext.getString(R.string.wallet_secondary_label_device_locked);
-                    state.sideViewCustomDrawable = null;
-                } else {
-                    state.state = Tile.STATE_ACTIVE;
-                    state.secondaryLabel = mSelectedCard.getContentDescription();
-                    state.sideViewCustomDrawable = mCardViewDrawable;
-                }
+                state.state = isDeviceLocked ? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE;
+                state.secondaryLabel = mSelectedCard.getContentDescription();
+                state.sideViewCustomDrawable = mCardViewDrawable;
             } else {
                 state.state = Tile.STATE_INACTIVE;
                 state.secondaryLabel =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
index f4dd415..d99c1d1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
@@ -92,15 +92,15 @@
 
     @Override
     protected void handleClick(@Nullable View view) {
-        if (mKeyguard.isMethodSecure() && mKeyguard.isShowing()) {
-            mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
-                mSensorPrivacyController.setSensorBlocked(QS_TILE, getSensorId(),
-                        !mSensorPrivacyController.isSensorBlocked(getSensorId()));
-            });
+        boolean blocked = mSensorPrivacyController.isSensorBlocked(getSensorId());
+        if (mSensorPrivacyController.requiresAuthentication()
+                && mKeyguard.isMethodSecure()
+                && mKeyguard.isShowing()) {
+            mActivityStarter.postQSRunnableDismissingKeyguard(() ->
+                    mSensorPrivacyController.setSensorBlocked(QS_TILE, getSensorId(), !blocked));
             return;
         }
-        mSensorPrivacyController.setSensorBlocked(QS_TILE, getSensorId(),
-                !mSensorPrivacyController.isSensorBlocked(getSensorId()));
+        mSensorPrivacyController.setSensorBlocked(QS_TILE, getSensorId(), !blocked);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
index dae375a..2d1d8b7 100644
--- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
@@ -134,7 +134,9 @@
     override fun onClick(dialog: DialogInterface?, which: Int) {
         when (which) {
             BUTTON_POSITIVE -> {
-                if (keyguardStateController.isMethodSecure && keyguardStateController.isShowing) {
+                if (sensorPrivacyController.requiresAuthentication() &&
+                        keyguardStateController.isMethodSecure &&
+                        keyguardStateController.isShowing) {
                     keyguardDismissUtil.executeWhenUnlocked({
                         bgHandler.postDelayed({
                             disableSensorPrivacy()
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserContextProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/UserContextProvider.kt
index 27af152..dae8512 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserContextProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserContextProvider.kt
@@ -23,4 +23,10 @@
  */
 interface UserContextProvider {
     val userContext: Context
+
+    /**
+     * Creates the {@code context} with the current user.
+     * @see Context#createContextAsUser(UserHandle, int)
+     */
+    fun createCurrentUserContext(context: Context): Context
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 3a6248b..80d5f16 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -127,6 +127,12 @@
         }
     }
 
+    override fun createCurrentUserContext(context: Context): Context {
+        synchronized(mutex) {
+            return context.createContextAsUser(userHandle, 0)
+        }
+    }
+
     private fun setUserIdInternal(user: Int): Pair<Context, List<UserInfo>> {
         val profiles = userManager.getProfiles(user)
         val handle = UserHandle(user)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index ebd610b..0c9e1ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -180,13 +180,6 @@
     default void setRequestTopUi(boolean requestTopUi, String componentTag) {}
 
     /**
-     * Under low light conditions, we might want to increase the display brightness on devices that
-     * don't have an IR camera.
-     * @param brightness float from 0 to 1 or {@code LayoutParams.BRIGHTNESS_OVERRIDE_NONE}
-     */
-    default void setFaceAuthDisplayBrightness(float brightness) {}
-
-    /**
      * If {@link LightRevealScrim} obscures the UI.
      * @param opaque if the scrim is opaque
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt
index a390e9f..15ad312 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt
@@ -20,11 +20,13 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.render.NodeController
 import com.android.systemui.statusbar.notification.dagger.PeopleHeader
+import com.android.systemui.statusbar.notification.icon.ConversationIconManager
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.PeopleNotificationType
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
@@ -39,12 +41,40 @@
 @CoordinatorScope
 class ConversationCoordinator @Inject constructor(
     private val peopleNotificationIdentifier: PeopleNotificationIdentifier,
+    private val conversationIconManager: ConversationIconManager,
     @PeopleHeader peopleHeaderController: NodeController
 ) : Coordinator {
 
+    private val promotedEntriesToSummaryOfSameChannel =
+        mutableMapOf<NotificationEntry, NotificationEntry>()
+
+    private val onBeforeRenderListListener = OnBeforeRenderListListener { _ ->
+        val unimportantSummaries = promotedEntriesToSummaryOfSameChannel
+            .mapNotNull { (promoted, summary) ->
+                val originalGroup = summary.parent
+                when {
+                    originalGroup == null -> null
+                    originalGroup == promoted.parent -> null
+                    originalGroup.parent == null -> null
+                    originalGroup.summary != summary -> null
+                    originalGroup.children.any { it.channel == summary.channel } -> null
+                    else -> summary.key
+                }
+            }
+        conversationIconManager.setUnimportantConversations(unimportantSummaries)
+        promotedEntriesToSummaryOfSameChannel.clear()
+    }
+
     private val notificationPromoter = object : NotifPromoter(TAG) {
         override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean {
-            return entry.channel?.isImportantConversation == true
+            val shouldPromote = entry.channel?.isImportantConversation == true
+            if (shouldPromote) {
+                val summary = entry.parent?.summary
+                if (summary != null && entry.channel == summary.channel) {
+                    promotedEntriesToSummaryOfSameChannel[entry] = summary
+                }
+            }
+            return shouldPromote
         }
     }
 
@@ -67,6 +97,7 @@
 
     override fun attach(pipeline: NotifPipeline) {
         pipeline.addPromoter(notificationPromoter)
+        pipeline.addOnBeforeRenderListListener(onBeforeRenderListListener)
     }
 
     private fun isConversation(entry: ListEntry): Boolean =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
index 386e2d3..f460644 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
@@ -18,7 +18,6 @@
 
 import android.annotation.MainThread
 import android.view.View
-import com.android.systemui.util.kotlin.transform
 import com.android.systemui.util.traceSection
 
 /**
@@ -41,7 +40,6 @@
 ) {
     private val rootNode = ShadeNode(rootController)
     private val nodes = mutableMapOf(rootController to rootNode)
-    private val views = mutableMapOf<View, ShadeNode>()
 
     /**
      * Adds and removes views from the root (and its children) until their structure matches the
@@ -66,26 +64,25 @@
      *
      * For debugging purposes.
      */
-    fun getViewLabel(view: View): String = views[view]?.label ?: view.toString()
+    fun getViewLabel(view: View): String =
+            nodes.values.firstOrNull { node -> node.view === view }?.label ?: view.toString()
 
-    private fun detachChildren(
-        parentNode: ShadeNode,
-        specMap: Map<NodeController, NodeSpec>
-    ) {
-        val parentSpec = specMap[parentNode.controller]
-
-        for (i in parentNode.getChildCount() - 1 downTo 0) {
-            val childView = parentNode.getChildAt(i)
-            views[childView]?.let { childNode ->
-                val childSpec = specMap[childNode.controller]
-
-                maybeDetachChild(parentNode, parentSpec, childNode, childSpec)
-
-                if (childNode.controller.getChildCount() > 0) {
-                    detachChildren(childNode, specMap)
+    private fun detachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) {
+        val views = nodes.values.associateBy { it.view }
+        fun detachRecursively(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) {
+            val parentSpec = specMap[parentNode.controller]
+            for (i in parentNode.getChildCount() - 1 downTo 0) {
+                val childView = parentNode.getChildAt(i)
+                views[childView]?.let { childNode ->
+                    val childSpec = specMap[childNode.controller]
+                    maybeDetachChild(parentNode, parentSpec, childNode, childSpec)
+                    if (childNode.controller.getChildCount() > 0) {
+                        detachRecursively(childNode, specMap)
+                    }
                 }
             }
         }
+        detachRecursively(parentNode, specMap)
     }
 
     private fun maybeDetachChild(
@@ -94,14 +91,13 @@
         childNode: ShadeNode,
         childSpec: NodeSpec?
     ) {
-        val newParentNode = transform(childSpec?.parent) { getNode(it) }
+        val newParentNode = childSpec?.parent?.let { getNode(it) }
 
         if (newParentNode != parentNode) {
             val childCompletelyRemoved = newParentNode == null
 
             if (childCompletelyRemoved) {
                 nodes.remove(childNode.controller)
-                views.remove(childNode.controller.view)
             }
 
             logger.logDetachingChild(
@@ -115,10 +111,7 @@
         }
     }
 
-    private fun attachChildren(
-        parentNode: ShadeNode,
-        specMap: Map<NodeController, NodeSpec>
-    ) {
+    private fun attachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) {
         val parentSpec = checkNotNull(specMap[parentNode.controller])
 
         for ((index, childSpec) in parentSpec.children.withIndex()) {
@@ -160,7 +153,6 @@
         if (node == null) {
             node = ShadeNode(spec.controller)
             nodes[node.controller] = node
-            views[node.view] = node
         }
         return node
     }
@@ -194,10 +186,9 @@
 
 private class DuplicateNodeException(message: String) : RuntimeException(message)
 
-private class ShadeNode(
-    val controller: NodeController
-) {
-    val view = controller.view
+private class ShadeNode(val controller: NodeController) {
+    val view: View
+        get() = controller.view
 
     var parent: ShadeNode? = null
 
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 34c8044..d96590a 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
@@ -70,6 +70,8 @@
 import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
 import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.icon.ConversationIconManager;
+import com.android.systemui.statusbar.notification.icon.IconManager;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.init.NotificationsControllerImpl;
 import com.android.systemui.statusbar.notification.init.NotificationsControllerStub;
@@ -370,6 +372,10 @@
 
     /** */
     @Binds
+    ConversationIconManager bindConversationIconManager(IconManager iconManager);
+
+    /** */
+    @Binds
     BindEventManager bindBindEventManagerImpl(BindEventManagerImpl bindEventManagerImpl);
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 5375ac3..d896541 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -27,6 +27,7 @@
 import android.widget.ImageView
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.InflationException
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -44,11 +45,14 @@
  * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry.
  *  Long-term, it should probably live somewhere in the content inflation pipeline.
  */
+@SysUISingleton
 class IconManager @Inject constructor(
     private val notifCollection: CommonNotifCollection,
     private val launcherApps: LauncherApps,
     private val iconBuilder: IconBuilder
-) {
+) : ConversationIconManager {
+    private var unimportantConversationKeys: Set<String> = emptySet()
+
     fun attach() {
         notifCollection.addCollectionListener(entryListener)
     }
@@ -63,16 +67,8 @@
         }
 
         override fun onRankingApplied() {
-            // When the sensitivity changes OR when the isImportantConversation status changes,
-            // we need to update the icons
-            for (entry in notifCollection.allNotifs) {
-                val isImportant = isImportantConversation(entry)
-                if (entry.icons.areIconsAvailable &&
-                        isImportant != entry.icons.isImportantConversation) {
-                    updateIconsSafe(entry)
-                }
-                entry.icons.isImportantConversation = isImportant
-            }
+            // rankings affect whether a conversation is important, which can change the icons
+            recalculateForImportantConversationChange()
         }
     }
 
@@ -80,6 +76,18 @@
         entry -> updateIconsSafe(entry)
     }
 
+    private fun recalculateForImportantConversationChange() {
+        for (entry in notifCollection.allNotifs) {
+            val isImportant = isImportantConversation(entry)
+            if (entry.icons.areIconsAvailable &&
+                isImportant != entry.icons.isImportantConversation
+            ) {
+                updateIconsSafe(entry)
+            }
+            entry.icons.isImportantConversation = isImportant
+        }
+    }
+
     /**
      * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the
      * result in [NotificationEntry.getIcons].
@@ -306,8 +314,28 @@
     }
 
     private fun isImportantConversation(entry: NotificationEntry): Boolean {
-        return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation
+        return entry.ranking.channel != null &&
+                entry.ranking.channel.isImportantConversation &&
+                entry.key !in unimportantConversationKeys
+    }
+
+    override fun setUnimportantConversations(keys: Collection<String>) {
+        val newKeys = keys.toSet()
+        val changed = unimportantConversationKeys != newKeys
+        unimportantConversationKeys = newKeys
+        if (changed) {
+            recalculateForImportantConversationChange()
+        }
     }
 }
 
-private const val TAG = "IconManager"
\ No newline at end of file
+private const val TAG = "IconManager"
+
+interface ConversationIconManager {
+    /**
+     * Sets the complete current set of notification keys which should (for the purposes of icon
+     * presentation) be considered unimportant.  This tells the icon manager to remove the avatar
+     * of a group from which the priority notification has been removed.
+     */
+    fun setUnimportantConversations(keys: Collection<String>)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 5646545..cff42f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -18,6 +18,8 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider
@@ -72,7 +74,7 @@
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val highPriorityProvider: HighPriorityProvider,
-    private val statusBarStateController: StatusBarStateController,
+    private val statusBarStateController: SysuiStatusBarStateController,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val secureSettings: SecureSettings,
     private val globalSettings: GlobalSettings
@@ -105,7 +107,8 @@
                 if (uri == showSilentNotifsUri) {
                     readShowSilentNotificationSetting()
                 }
-                if (keyguardStateController.isShowing) {
+                if (statusBarStateController.getCurrentOrUpcomingState()
+                        == StatusBarState.KEYGUARD) {
                     notifyStateChanged("Settings $uri changed")
                 }
             }
@@ -131,13 +134,14 @@
 
         // register (maybe) public mode changed callbacks:
         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onStateChanged(state: Int) {
-                notifyStateChanged("onStatusBarStateChanged")
+            override fun onUpcomingStateChanged(state: Int) {
+                notifyStateChanged("onStatusBarUpcomingStateChanged")
             }
         })
         broadcastDispatcher.registerReceiver(object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
-                if (keyguardStateController.isShowing) {
+                if (statusBarStateController.getCurrentOrUpcomingState()
+                        == StatusBarState.KEYGUARD) {
                     // maybe public mode changed
                     notifyStateChanged(intent.action!!)
                 }
@@ -159,17 +163,28 @@
 
     override fun shouldHideNotification(entry: NotificationEntry): Boolean = when {
         // Keyguard state doesn't matter if the keyguard is not showing.
-        !keyguardStateController.isShowing -> false
+        statusBarStateController.getCurrentOrUpcomingState() != StatusBarState.KEYGUARD -> false
         // Notifications not allowed on the lockscreen, always hide.
         !lockscreenUserManager.shouldShowLockscreenNotifications() -> true
         // User settings do not allow this notification on the lockscreen, so hide it.
         userSettingsDisallowNotification(entry) -> true
+        // if entry is silent, apply custom logic to see if should hide
+        shouldHideIfEntrySilent(entry) -> true
+        else -> false
+    }
+
+    private fun shouldHideIfEntrySilent(entry: ListEntry): Boolean = when {
+        // Show if high priority (not hidden)
+        highPriorityProvider.isHighPriority(entry) -> false
+        // Ambient notifications are hidden always from lock screen
+        entry.representativeEntry?.isAmbient == true -> true
+        // [Now notification is silent]
+        // Hide regardless of parent priority if user wants silent notifs hidden
+        hideSilentNotificationsOnLockscreen -> true
         // Parent priority is high enough to be shown on the lockscreen, do not hide.
-        entry.parent?.let(::priorityExceedsLockscreenShowingThreshold) == true -> false
-        // Entry priority is high enough to be shown on the lockscreen, do not hide.
-        priorityExceedsLockscreenShowingThreshold(entry) -> false
-        // Priority is too low, hide.
-        else -> true
+        entry.parent?.let(::shouldHideIfEntrySilent) == false -> false
+        // Show when silent notifications are allowed on lockscreen
+        else -> false
     }
 
     private fun userSettingsDisallowNotification(entry: NotificationEntry): Boolean {
@@ -193,11 +208,6 @@
         }
     }
 
-    private fun priorityExceedsLockscreenShowingThreshold(entry: ListEntry): Boolean = when {
-        hideSilentNotificationsOnLockscreen -> highPriorityProvider.isHighPriority(entry)
-        else -> entry.representativeEntry?.ranking?.isAmbient == false
-    }
-
     private fun readShowSilentNotificationSetting() {
         val showSilentNotifs =
                 secureSettings.getBoolForUser(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 36cd173..3ea5e5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -70,6 +70,7 @@
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.policy.SystemBarUtils;
+import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.keyguard.KeyguardSliceView;
 import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
@@ -1313,7 +1314,10 @@
         final float endTopPosition = mTopPadding + mExtraTopInsetForFullShadeTransition
                 + mAmbientState.getOverExpansion()
                 - getCurrentOverScrollAmount(false /* top */);
-        final float fraction = mAmbientState.getExpansionFraction();
+        float fraction = mAmbientState.getExpansionFraction();
+        if (mAmbientState.isBouncerInTransit()) {
+            fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
+        }
         final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
         mAmbientState.setStackY(stackY);
         if (mOnStackYChanged != null) {
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 ef24d77..5c12671 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1022,6 +1022,23 @@
             public void onUnlockedChanged() {
                 logStateToEventlog();
             }
+
+            @Override
+            public void onKeyguardGoingAwayChanged() {
+                // The light reveal scrim should always be fully revealed by the time the keyguard
+                // is done going away. Double check that this is true.
+                if (!mKeyguardStateController.isKeyguardGoingAway()) {
+                    if (mLightRevealScrim.getRevealAmount() != 1f) {
+                        Log.e(TAG, "Keyguard is done going away, but someone left the light reveal "
+                                + "scrim at reveal amount: " + mLightRevealScrim.getRevealAmount());
+                    }
+
+                    // If the auth ripple is still playing, let it finish.
+                    if (!mAuthRippleController.isAnimatingLightRevealScrim()) {
+                        mLightRevealScrim.setRevealAmount(1f);
+                    }
+                }
+            }
         });
         startKeyguard();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 69beaf56..0b72138 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -220,7 +220,7 @@
                 DejankUtils.postAfterTraversal(mShowRunnable);
             }
 
-            mCallback.onBouncerVisiblityChanged(true /* shown */);
+            mKeyguardStateController.notifyBouncerShowing(true /* showing */);
             dispatchStartingToShow();
         } finally {
             Trace.endSection();
@@ -334,7 +334,7 @@
         }
         mIsScrimmed = false;
         mFalsingCollector.onBouncerHidden();
-        mCallback.onBouncerVisiblityChanged(false /* shown */);
+        mKeyguardStateController.notifyBouncerShowing(false /* showing */);
         cancelShowRunnable();
         if (mKeyguardViewController != null) {
             mKeyguardViewController.cancelDismissAction();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
index 24660b2..faae4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
@@ -111,7 +111,6 @@
 
     private final SysuiColorExtractor mColorExtractor;
     private final ScreenOffAnimationController mScreenOffAnimationController;
-    private float mFaceAuthDisplayBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
     /**
      * Layout params would be aggregated and dispatched all at once if this is > 0.
      *
@@ -266,12 +265,6 @@
         mScreenBrightnessDoze = value / 255f;
     }
 
-    @Override
-    public void setFaceAuthDisplayBrightness(float brightness) {
-        mFaceAuthDisplayBrightness = brightness;
-        apply(mCurrentState);
-    }
-
     private void setKeyguardDark(boolean dark) {
         int vis = mNotificationShadeView.getSystemUiVisibility();
         if (dark) {
@@ -340,7 +333,7 @@
     }
 
     private void adjustScreenOrientation(State state) {
-        if (state.isKeyguardShowingAndNotOccluded() || state.mDozing) {
+        if (state.mBouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.mDozing) {
             if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) {
                 mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
             } else {
@@ -455,7 +448,9 @@
 
     private void applyWindowLayoutParams() {
         if (mDeferWindowLayoutParams == 0 && mLp != null && mLp.copyFrom(mLpChanged) != 0) {
+            Trace.beginSection("updateViewLayout");
             mWindowManager.updateViewLayout(mNotificationShadeView, mLp);
+            Trace.endSection();
         }
     }
 
@@ -523,7 +518,7 @@
         if (state.mForceDozeBrightness) {
             mLpChanged.screenBrightness = mScreenBrightnessDoze;
         } else {
-            mLpChanged.screenBrightness = mFaceAuthDisplayBrightness;
+            mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
         }
     }
 
@@ -572,6 +567,10 @@
 
     @Override
     public void setPanelVisible(boolean visible) {
+        if (mCurrentState.mPanelVisible == visible
+                && mCurrentState.mNotificationShadeFocusable == visible) {
+            return;
+        }
         mCurrentState.mPanelVisible = visible;
         mCurrentState.mNotificationShadeFocusable = visible;
         apply(mCurrentState);
@@ -626,8 +625,14 @@
 
     @Override
     public void setScrimsVisibility(int scrimsVisibility) {
+        if (scrimsVisibility == mCurrentState.mScrimsVisibility) {
+            return;
+        }
+        boolean wasExpanded = isExpanded(mCurrentState);
         mCurrentState.mScrimsVisibility = scrimsVisibility;
-        apply(mCurrentState);
+        if (wasExpanded != isExpanded(mCurrentState)) {
+            apply(mCurrentState);
+        }
         mScrimsVisibilityListener.accept(scrimsVisibility);
     }
 
@@ -687,6 +692,9 @@
 
     @Override
     public void setPanelExpanded(boolean isExpanded) {
+        if (mCurrentState.mPanelExpanded == isExpanded) {
+            return;
+        }
         mCurrentState.mPanelExpanded = isExpanded;
         apply(mCurrentState);
     }
@@ -703,6 +711,9 @@
      */
     @Override
     public void setForceDozeBrightness(boolean forceDozeBrightness) {
+        if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) {
+            return;
+        }
         mCurrentState.mForceDozeBrightness = forceDozeBrightness;
         apply(mCurrentState);
     }
@@ -841,7 +852,6 @@
         boolean mLightRevealScrimOpaque;
         boolean mForceCollapsed;
         boolean mForceDozeBrightness;
-        int mFaceAuthDisplayBrightness;
         boolean mForceUserActivity;
         boolean mLaunchingActivity;
         boolean mBackdropShowing;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 935f87d..c1d0769 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -175,28 +175,36 @@
                 .setDuration(duration.toLong())
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .alpha(1f)
+                .withEndAction {
+                    aodUiAnimationPlaying = false
+
+                    // Lock the keyguard if it was waiting for the screen off animation to end.
+                    keyguardViewMediatorLazy.get().maybeHandlePendingLock()
+
+                    // Tell the CentralSurfaces to become keyguard for real - we waited on that
+                    // since it is slow and would have caused the animation to jank.
+                    mCentralSurfaces.updateIsKeyguard()
+
+                    // Run the callback given to us by the KeyguardVisibilityHelper.
+                    after.run()
+
+                    // Done going to sleep, reset this flag.
+                    decidedToAnimateGoingToSleep = null
+
+                    // We need to unset the listener. These are persistent for future animators
+                    keyguardView.animate().setListener(null)
+                    interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
+                }
                 .setListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
-                        aodUiAnimationPlaying = false
-
-                        // Lock the keyguard if it was waiting for the screen off animation to end.
-                        keyguardViewMediatorLazy.get().maybeHandlePendingLock()
-
-                        // Tell the CentralSurfaces to become keyguard for real - we waited on that
-                        // since it is slow and would have caused the animation to jank.
-                        mCentralSurfaces.updateIsKeyguard()
-
-                        // Run the callback given to us by the KeyguardVisibilityHelper.
-                        after.run()
-
-                        // Done going to sleep, reset this flag.
-                        decidedToAnimateGoingToSleep = null
-                        // We need to unset the listener. These are persistent for future animators
-                        keyguardView.animate().setListener(null)
-                        interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
-                    }
-
                     override fun onAnimationCancel(animation: Animator?) {
+                        // If we're cancelled, reset state flags/listeners. The end action above
+                        // will not be called, which is what we want since that will finish the
+                        // screen off animation and show the lockscreen, which we don't want if we
+                        // were cancelled.
+                        aodUiAnimationPlaying = false
+                        decidedToAnimateGoingToSleep = null
+                        keyguardView.animate().setListener(null)
+
                         interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
index 1e73d59..eb08f37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
@@ -37,6 +37,11 @@
 
     void suppressSensorPrivacyReminders(int sensor, boolean suppress);
 
+    /**
+     * @return whether lock screen authentication is required to change the toggle state
+     */
+    boolean requiresAuthentication();
+
     interface Callback {
         void onSensorBlockedChanged(@Sensor int sensor, boolean blocked);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index e4c444d..fffd839 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -37,6 +37,7 @@
     private final @NonNull SensorPrivacyManager mSensorPrivacyManager;
     private final SparseBooleanArray mSoftwareToggleState = new SparseBooleanArray();
     private final SparseBooleanArray mHardwareToggleState = new SparseBooleanArray();
+    private Boolean mRequiresAuthentication;
     private final Set<Callback> mCallbacks = new ArraySet<>();
 
     public IndividualSensorPrivacyControllerImpl(
@@ -96,6 +97,11 @@
     }
 
     @Override
+    public boolean requiresAuthentication() {
+        return mSensorPrivacyManager.requiresAuthentication();
+    }
+
+    @Override
     public void addCallback(@NonNull Callback listener) {
         mCallbacks.add(listener);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index 233778d..15ee553 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -46,6 +46,11 @@
     boolean isShowing();
 
     /**
+     * Whether the bouncer (PIN/password entry) is currently visible.
+     */
+    boolean isBouncerShowing();
+
+    /**
      * If swiping up will unlock without asking for a password.
      * @see #isUnlocked()
      */
@@ -186,6 +191,8 @@
     default void notifyKeyguardDoneFading() {}
     /** **/
     default void notifyKeyguardState(boolean showing, boolean occluded) {}
+    /** **/
+    default void notifyBouncerShowing(boolean showing) {}
 
     /**
      * Updates the keyguard state to reflect that it's in the process of being dismissed, either by
@@ -231,6 +238,11 @@
         default void onKeyguardShowingChanged() {}
 
         /**
+         * Called when the bouncer (PIN/password entry) is shown or hidden.
+         */
+        default void onBouncerShowingChanged() {}
+
+        /**
          * Triggered when the device was just unlocked and the lock screen is being dismissed.
          */
         default void onKeyguardFadingAwayChanged() {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index be5da37..77e285d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -63,6 +63,7 @@
 
     private boolean mCanDismissLockScreen;
     private boolean mShowing;
+    private boolean mBouncerShowing;
     private boolean mSecure;
     private boolean mOccluded;
 
@@ -153,6 +154,11 @@
     }
 
     @Override
+    public boolean isBouncerShowing() {
+        return mBouncerShowing;
+    }
+
+    @Override
     public boolean isMethodSecure() {
         return mSecure;
     }
@@ -328,6 +334,15 @@
     }
 
     @Override
+    public void notifyBouncerShowing(boolean showing) {
+        if (mBouncerShowing != showing) {
+            mBouncerShowing = showing;
+
+            new ArrayList<>(mCallbacks).forEach(Callback::onBouncerShowingChanged);
+        }
+    }
+
+    @Override
     public void notifyPanelFlingEnd() {
         mFlingingToDismissKeyguard = false;
         mFlingingToDismissKeyguardDuringSwipeGesture = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
index f26176c..f8c36dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -86,7 +86,6 @@
     private boolean mShouldDisplayAllAccesses;
     private boolean mShowSystemAccessesFlag;
     private boolean mShowSystemAccessesSetting;
-    private boolean mExperimentStarted;
 
     @Inject
     public LocationControllerImpl(Context context, AppOpsController appOpsController,
@@ -108,9 +107,6 @@
         mShouldDisplayAllAccesses = getAllAccessesSetting();
         mShowSystemAccessesFlag = getShowSystemFlag();
         mShowSystemAccessesSetting = getShowSystemSetting();
-        mExperimentStarted = getExperimentStarted();
-        toggleSystemSettingIfExperimentJustStarted();
-
         mContentObserver = new ContentObserver(mBackgroundHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -127,15 +123,8 @@
                 DeviceConfig.NAMESPACE_PRIVACY,
                 backgroundHandler::post,
                 properties -> {
-                    // Update the Device Config flag which controls the experiment to display
-                    // location accesses.
                     mShouldDisplayAllAccesses = getAllAccessesSetting();
-                    // Update the Device Config flag which controls the experiment to display
-                    // system location accesses.
-                    mShowSystemAccessesFlag = getShowSystemFlag();
-                    // Update the local flag for the system accesses experiment and potentially
-                    // update the behavior based on the flag value.
-                    toggleSystemSettingIfExperimentJustStarted();
+                    mShowSystemAccessesFlag = getShowSystemSetting();
                     updateActiveLocationRequests();
                 });
 
@@ -234,33 +223,6 @@
                 UserHandle.USER_CURRENT) == 1;
     }
 
-    private boolean getExperimentStarted() {
-        return mSecureSettings
-                .getInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED, 0) == 1;
-    }
-
-    private void toggleSystemSettingIfExperimentJustStarted() {
-        // mShowSystemAccessesFlag indicates whether the Device Config flag is flipped
-        // by an experiment. mExperimentStarted is the local device value which indicates the last
-        // value the device has seen for the Device Config flag.
-        // The local device value is needed to determine that the Device Config flag was just
-        // flipped, as the experiment behavior should only happen once after the experiment is
-        // enabled.
-        if (mShowSystemAccessesFlag && !mExperimentStarted) {
-            // If the Device Config flag is enabled, but the local device setting is not then the
-            // experiment just started. Update the local flag to match and enable the experiment
-            // behavior by flipping the show system setting value.
-            mSecureSettings.putInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED, 1);
-            mSecureSettings.putInt(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 1);
-            mExperimentStarted = true;
-        } else if (!mShowSystemAccessesFlag && mExperimentStarted) {
-            // If the Device Config flag is disabled, but the local device flag is enabled then
-            // update the local device flag to match.
-            mSecureSettings.putInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED, 0);
-            mExperimentStarted = false;
-        }
-    }
-
     /**
      * Returns true if there currently exist active high power location requests.
      */
@@ -288,6 +250,7 @@
         }
         boolean hadActiveLocationRequests = mAreActiveLocationRequests;
         boolean shouldDisplay = false;
+        boolean showSystem = mShowSystemAccessesFlag || mShowSystemAccessesSetting;
         boolean systemAppOp = false;
         boolean nonSystemAppOp = false;
         boolean isSystemApp;
@@ -305,7 +268,7 @@
                     nonSystemAppOp = true;
                 }
 
-                shouldDisplay = mShowSystemAccessesSetting || shouldDisplay || !isSystemApp;
+                shouldDisplay = showSystem || shouldDisplay || !isSystemApp;
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 3751721..5a33603 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -269,8 +269,12 @@
                 super.onEnd(animation);
                 if (animation.getTypeMask() == WindowInsets.Type.ime()) {
                     mEntry.mRemoteEditImeAnimatingAway = false;
-                    mEntry.mRemoteEditImeVisible =
-                            mEditText.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
+                    WindowInsets editTextRootWindowInsets = mEditText.getRootWindowInsets();
+                    if (editTextRootWindowInsets == null) {
+                        Log.w(TAG, "onEnd called on detached view", new Exception());
+                    }
+                    mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null
+                            && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime());
                     if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
                         mController.removeRemoteInput(mEntry, mToken);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java
index 5b5dca3..d54de3fa 100644
--- a/packages/SystemUI/src/com/android/systemui/util/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java
@@ -21,9 +21,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
-import android.content.res.TypedArray;
 import android.provider.Settings;
-import android.view.ContextThemeWrapper;
 import android.view.DisplayCutout;
 
 import com.android.internal.policy.SystemBarUtils;
@@ -35,6 +33,8 @@
 
 public class Utils {
 
+    private static Boolean sUseQsMediaPlayer = null;
+
     /**
      * Allows lambda iteration over a list. It is done in reverse order so it is safe
      * to add or remove items during the iteration.  Skips over null items.
@@ -81,9 +81,16 @@
      * Off by default, but can be disabled by setting to 0
      */
     public static boolean useQsMediaPlayer(Context context) {
-        int flag = Settings.Global.getInt(context.getContentResolver(),
-                Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
-        return flag > 0;
+        // TODO(b/192412820): Replace SHOW_MEDIA_ON_QUICK_SETTINGS with compile-time value
+        // Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS can't be toggled at runtime, so simply
+        // cache the first result we fetch and use that going forward. Do this to avoid unnecessary
+        // binder calls which may happen on the critical path.
+        if (sUseQsMediaPlayer == null) {
+            int flag = Settings.Global.getInt(context.getContentResolver(),
+                    Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
+            sUseQsMediaPlayer = flag > 0;
+        }
+        return sUseQsMediaPlayer;
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index aff94eb..a1e23a20 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.util.concurrency.DelayableExecutor
 import org.junit.Before
 import org.junit.Test
@@ -63,7 +62,7 @@
     @Mock
     lateinit var falsingCollector: FalsingCollector
     @Mock
-    lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    lateinit var keyguardViewController: KeyguardViewController
     @Mock
     private lateinit var mKeyguardMessageArea: KeyguardMessageArea
     @Mock
@@ -92,13 +91,13 @@
                 mainExecutor,
                 mContext.resources,
                 falsingCollector,
-                statusBarKeyguardViewManager
+                keyguardViewController
         )
     }
 
     @Test
     fun testFocusWhenBouncerIsShown() {
-        Mockito.`when`(statusBarKeyguardViewManager.isBouncerShowing).thenReturn(true)
+        Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
         Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
         keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
         keyguardPasswordView.post { verify(keyguardPasswordView).requestFocus() }
@@ -106,7 +105,7 @@
 
     @Test
     fun testDoNotFocusWhenBouncerIsHidden() {
-        Mockito.`when`(statusBarKeyguardViewManager.isBouncerShowing).thenReturn(false)
+        Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
         Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
         keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
         verify(keyguardPasswordView, never()).requestFocus()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 7b7dfdc..4d33430 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -51,7 +51,6 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -125,7 +124,7 @@
     @Mock
     private SessionTracker mSessionTracker;
     @Mock
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private KeyguardViewController mKeyguardViewController;
     private Configuration mConfiguration;
 
     private KeyguardSecurityContainerController mKeyguardSecurityContainerController;
@@ -153,7 +152,7 @@
                 (KeyguardPasswordView) mKeyguardPasswordView, mKeyguardUpdateMonitor,
                 SecurityMode.Password, mLockPatternUtils, null,
                 mKeyguardMessageAreaControllerFactory, null, null, mEmergencyButtonController,
-                null, mock(Resources.class), null, mStatusBarKeyguardViewManager);
+                null, mock(Resources.class), null, mKeyguardViewController);
 
         mKeyguardSecurityContainerController = new KeyguardSecurityContainerController.Factory(
                 mView, mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 57f3617..92c2a1b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -29,6 +29,7 @@
     @FingerprintSensorProperties.SensorType sensorType: Int =
         FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
     maxEnrollmentsPerUser: Int = 1,
+    halControlsIllumination: Boolean = true,
     info: List<ComponentInfoInternal> = listOf(ComponentInfoInternal("a", "b", "c", "d", "e")),
     resetLockoutRequiresHardwareAuthToken: Boolean = false
 ) = FingerprintSensorPropertiesInternal(
@@ -37,6 +38,7 @@
     maxEnrollmentsPerUser,
     info,
     sensorType,
+    halControlsIllumination,
     resetLockoutRequiresHardwareAuthToken,
     listOf(this)
 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
index 102f37c..dec2b82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
@@ -185,6 +185,7 @@
                     5 /* maxEnrollmentsPerUser */,
                     listOf() /* componentInfo */,
                     FingerprintSensorProperties.TYPE_POWER_BUTTON,
+                    true /* halControlsIllumination */,
                     true /* resetLockoutRequiresHardwareAuthToken */,
                     locations
                 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index a57b011..fc5ccbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -65,6 +65,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
 
+private const val HAL_CONTROLS_ILLUMINATION = true
 private const val REQUEST_ID = 2L
 
 // Dimensions for the current display resolution.
@@ -129,8 +130,8 @@
             statusBarStateController, panelExpansionStateManager, statusBarKeyguardViewManager,
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
-            unlockedScreenOffAnimationController, hbmProvider, REQUEST_ID, reason,
-            controllerCallback, onTouch, activityLaunchAnimator)
+            unlockedScreenOffAnimationController, HAL_CONTROLS_ILLUMINATION, hbmProvider,
+            REQUEST_ID, reason, controllerCallback, onTouch, activityLaunchAnimator)
         block()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
index 27755ede..cd646c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
@@ -62,6 +62,7 @@
                 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
                 componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+                true /* halControlsIllumination */,
                 true /* resetLockoutRequiresHardwareAuthToken */,
                 List.of(new SensorLocationInternal("" /* displayId */,
                         sensorLocationX, sensorLocationY, sensorRadius)));
@@ -127,6 +128,7 @@
                 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
                 componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+                true /* halControlsIllumination */,
                 true /* resetLockoutRequiresHardwareAuthToken */,
                 List.of(new SensorLocationInternal("" /* displayId */,
                         sensorLocationX, sensorLocationY, sensorRadius)));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index 744af58..0327cfc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -36,6 +36,7 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.nullable
@@ -43,7 +44,6 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
 
-private const val DISPLAY_ID = "" // default display id
 private const val SENSOR_X = 50
 private const val SENSOR_Y = 250
 private const val SENSOR_RADIUS = 10
@@ -146,7 +146,7 @@
         view.startIllumination(onDone)
 
         val illuminator = withArgCaptor<Runnable> {
-            verify(hbmProvider).enableHbm(capture())
+            verify(hbmProvider).enableHbm(anyBoolean(), capture())
         }
 
         assertThat(view.isIlluminationRequested).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 2d8c4d5..2abc666 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -14,6 +14,7 @@
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import junit.framework.Assert.assertEquals
@@ -49,6 +50,8 @@
     private lateinit var biometricUnlockController: BiometricUnlockController
     @Mock
     private lateinit var surfaceTransactionApplier: SyncRtSurfaceTransactionApplier
+    @Mock
+    private lateinit var statusBarStateController: SysuiStatusBarStateController
 
     private lateinit var remoteAnimationTarget: RemoteAnimationTarget
 
@@ -57,7 +60,7 @@
         MockitoAnnotations.initMocks(this)
         keyguardUnlockAnimationController = KeyguardUnlockAnimationController(
             context, keyguardStateController, { keyguardViewMediator }, keyguardViewController,
-            featureFlags, { biometricUnlockController }
+            featureFlags, { biometricUnlockController }, statusBarStateController
         )
 
         `when`(keyguardViewController.viewRootImpl).thenReturn(mock(ViewRootImpl::class.java))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
index be923a6..5ff316e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
@@ -413,6 +413,7 @@
                         /* max enrollments per user */ 5,
                         /* component info */ new ArrayList<>(),
                         /* sensorType */ 3,
+                        /* halControlsIllumination */ true,
                         /* resetLockoutRequiresHwToken */ false,
                         List.of(new SensorLocationInternal("" /* displayId */,
                                 (int) udfpsLocation.x, (int) udfpsLocation.y, radius)));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
index 06d45de..a074475 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
@@ -27,6 +27,7 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -136,6 +137,24 @@
     }
 
     @Test
+    fun testConnection_calledTwice_oldBrowserDisconnected() {
+        val oldBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
+
+        // When testConnection can connect to the service
+        setupBrowserConnection()
+        resumeBrowser.testConnection()
+
+        // And testConnection is called again
+        val newBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
+        resumeBrowser.testConnection()
+
+        // Then we disconnect the old browser
+        verify(oldBrowser).disconnect()
+    }
+
+    @Test
     fun testFindRecentMedia_connectionFails_error() {
         // When findRecentMedia is called and we cannot connect
         setupBrowserFailed()
@@ -169,6 +188,24 @@
     }
 
     @Test
+    fun testFindRecentMedia_calledTwice_oldBrowserDisconnected() {
+        val oldBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
+
+        // When findRecentMedia is called and we connect
+        setupBrowserConnection()
+        resumeBrowser.findRecentMedia()
+
+        // And findRecentMedia is called again
+        val newBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
+        resumeBrowser.findRecentMedia()
+
+        // Then we disconnect the old browser
+        verify(oldBrowser).disconnect()
+    }
+
+    @Test
     fun testFindRecentMedia_noChildren_error() {
         // When findRecentMedia is called and we connect, but do not get any results
         setupBrowserConnectionNoResults()
@@ -223,6 +260,24 @@
         verify(transportControls).play()
     }
 
+    @Test
+    fun testRestart_calledTwice_oldBrowserDisconnected() {
+        val oldBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
+
+        // When restart is called and we connect successfully
+        setupBrowserConnection()
+        resumeBrowser.restart()
+
+        // And restart is called again
+        val newBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
+        resumeBrowser.restart()
+
+        // Then we disconnect the old browser
+        verify(oldBrowser).disconnect()
+    }
+
     /**
      * Helper function to mock a failed connection
      */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 3c6aa48..7c30398 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -85,6 +85,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -187,6 +188,8 @@
     private DeadZone mDeadZone;
     @Mock
     private CentralSurfaces mCentralSurfaces;
+    @Mock
+    private UserContextProvider mUserContextProvider;
     private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
 
     @Rule
@@ -210,6 +213,8 @@
         when(mNavigationBarTransitions.getLightTransitionsController())
                 .thenReturn(mLightBarTransitionsController);
         when(mStatusBarKeyguardViewManager.isNavBarVisible()).thenReturn(true);
+        when(mUserContextProvider.createCurrentUserContext(any(Context.class)))
+                .thenReturn(mContext);
         setupSysuiDependency();
         // This class inflates views that call Dependency.get, thus these injections are still
         // necessary.
@@ -458,7 +463,8 @@
                 mDeadZone,
                 mDeviceConfigProxyFake,
                 mNavigationBarTransitions,
-                Optional.of(mock(BackAnimation.class))));
+                Optional.of(mock(BackAnimation.class)),
+                mUserContextProvider));
     }
 
     private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
index c88ceac..4f6475f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
@@ -90,6 +90,7 @@
 
     private static final String CARD_ID = "card_id";
     private static final String LABEL = "QAW";
+    private static final String CARD_DESCRIPTION = "•••• 1234";
     private static final Icon CARD_IMAGE =
             Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888));
 
@@ -282,9 +283,7 @@
         mTile.handleUpdateState(state, null);
 
         assertEquals(Tile.STATE_ACTIVE, state.state);
-        assertEquals(
-                "•••• 1234",
-                state.secondaryLabel);
+        assertEquals(CARD_DESCRIPTION, state.secondaryLabel);
         assertNotNull(state.stateDescription);
         assertNotNull(state.sideViewCustomDrawable);
     }
@@ -298,11 +297,9 @@
         mTile.handleUpdateState(state, null);
 
         assertEquals(Tile.STATE_INACTIVE, state.state);
-        assertEquals(
-                mContext.getString(R.string.wallet_secondary_label_device_locked),
-                state.secondaryLabel);
+        assertEquals(CARD_DESCRIPTION, state.secondaryLabel);
         assertNotNull(state.stateDescription);
-        assertNull(state.sideViewCustomDrawable);
+        assertNotNull(state.sideViewCustomDrawable);
     }
 
     @Test
@@ -314,9 +311,7 @@
         mTile.handleUpdateState(state, null);
 
         assertEquals(Tile.STATE_ACTIVE, state.state);
-        assertEquals(
-                "•••• 1234",
-                state.secondaryLabel);
+        assertEquals(CARD_DESCRIPTION, state.secondaryLabel);
         assertNotNull(state.stateDescription);
         assertNotNull(state.sideViewCustomDrawable);
     }
@@ -426,6 +421,6 @@
     private WalletCard createWalletCard(Context context) {
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
-        return new WalletCard.Builder(CARD_ID, CARD_IMAGE, "•••• 1234", pendingIntent).build();
+        return new WalletCard.Builder(CARD_ID, CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index 540d291..1cce3b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -21,10 +21,13 @@
 import android.testing.AndroidTestingRunner
 
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
 
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.DeviceConfigProxyFake
+import com.android.systemui.util.Utils
+import com.android.systemui.util.mockito.any
 
 import org.junit.After
 import org.junit.Assert.assertFalse
@@ -32,28 +35,33 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
     var manager: NotificationSectionsFeatureManager? = null
     val proxyFake = DeviceConfigProxyFake()
-    var originalQsMediaPlayer: Int = 0
+    private lateinit var staticMockSession: MockitoSession
 
     @Before
     public fun setup() {
         manager = NotificationSectionsFeatureManager(proxyFake, mContext)
         manager!!.clearCache()
-        originalQsMediaPlayer = Settings.Global.getInt(context.getContentResolver(),
-                Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
+        staticMockSession = ExtendedMockito.mockitoSession()
+            .mockStatic<Utils>(Utils::class.java)
+            .strictness(Strictness.LENIENT)
+            .startMocking()
+        `when`(Utils.useQsMediaPlayer(any())).thenReturn(false)
         Settings.Global.putInt(context.getContentResolver(),
                 Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 0)
     }
 
     @After
     public fun teardown() {
-        Settings.Global.putInt(context.getContentResolver(),
-                Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsMediaPlayer)
+        staticMockSession.finishMocking()
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
index b45d78d..4b458f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
@@ -88,4 +88,7 @@
         return this;
     }
 
+    public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) {
+        return groupEntry.getRawChildren();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
index 7692a05..742fcf5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
@@ -21,17 +21,22 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.render.NodeController
+import com.android.systemui.statusbar.notification.icon.ConversationIconManager
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
@@ -52,8 +57,10 @@
     private lateinit var promoter: NotifPromoter
     private lateinit var peopleSectioner: NotifSectioner
     private lateinit var peopleComparator: NotifComparator
+    private lateinit var beforeRenderListListener: OnBeforeRenderListListener
 
     @Mock private lateinit var pipeline: NotifPipeline
+    @Mock private lateinit var conversationIconManager: ConversationIconManager
     @Mock private lateinit var peopleNotificationIdentifier: PeopleNotificationIdentifier
     @Mock private lateinit var channel: NotificationChannel
     @Mock private lateinit var headerController: NodeController
@@ -66,7 +73,11 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        coordinator = ConversationCoordinator(peopleNotificationIdentifier, headerController)
+        coordinator = ConversationCoordinator(
+            peopleNotificationIdentifier,
+            conversationIconManager,
+            headerController
+        )
         whenever(channel.isImportantConversation).thenReturn(true)
 
         coordinator.attach(pipeline)
@@ -75,6 +86,9 @@
         promoter = withArgCaptor {
             verify(pipeline).addPromoter(capture())
         }
+        beforeRenderListListener = withArgCaptor {
+            verify(pipeline).addOnBeforeRenderListListener(capture())
+        }
 
         peopleSectioner = coordinator.sectioner
         peopleComparator = peopleSectioner.comparator!!
@@ -96,6 +110,25 @@
     }
 
     @Test
+    fun testPromotedImportantConversationsMakesSummaryUnimportant() {
+        val altChildA = NotificationEntryBuilder().setTag("A").build()
+        val altChildB = NotificationEntryBuilder().setTag("B").build()
+        val summary = NotificationEntryBuilder().setId(2).setChannel(channel).build()
+        val groupEntry = GroupEntryBuilder()
+            .setParent(GroupEntry.ROOT_ENTRY)
+            .setSummary(summary)
+            .setChildren(listOf(entry, altChildA, altChildB))
+            .build()
+        assertTrue(promoter.shouldPromoteToTopLevel(entry))
+        assertFalse(promoter.shouldPromoteToTopLevel(altChildA))
+        assertFalse(promoter.shouldPromoteToTopLevel(altChildB))
+        NotificationEntryBuilder.setNewParent(entry, GroupEntry.ROOT_ENTRY)
+        GroupEntryBuilder.getRawChildren(groupEntry).remove(entry)
+        beforeRenderListListener.onBeforeRenderList(listOf(entry, groupEntry))
+        verify(conversationIconManager).setUnimportantConversations(eq(listOf(summary.key)))
+    }
+
+    @Test
     fun testInPeopleSection() {
         whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry))
             .thenReturn(TYPE_PERSON)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
index cf99607..3a85972 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
@@ -22,11 +22,14 @@
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MIN;
 
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.notification.collection.EntryUtilKt.modifyEntry;
 import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.argThat;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
@@ -55,6 +58,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.RankingBuilder;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -90,7 +94,7 @@
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private HighPriorityProvider mHighPriorityProvider;
-    @Mock private StatusBarStateController mStatusBarStateController;
+    @Mock private SysuiStatusBarStateController mStatusBarStateController;
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
     private final FakeSettings mFakeSettings = new FakeSettings();
 
@@ -178,7 +182,7 @@
         Consumer<String> listener = mock(Consumer.class);
         mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener);
 
-        callback.onStateChanged(0);
+        callback.onUpcomingStateChanged(0);
 
         verify(listener).accept(anyString());
     }
@@ -199,7 +203,7 @@
         Consumer<String> listener = mock(Consumer.class);
         mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener);
 
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
         callback.onReceive(mContext, new Intent(Intent.ACTION_USER_SWITCHED));
 
         verify(listener).accept(anyString());
@@ -207,7 +211,7 @@
 
     @Test
     public void notifyListeners_onSettingChange_lockScreenShowNotifs() {
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
         Consumer<String> listener = mock(Consumer.class);
         mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener);
 
@@ -218,7 +222,7 @@
 
     @Test
     public void notifyListeners_onSettingChange_lockScreenAllowPrivateNotifs() {
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
         Consumer<String> listener = mock(Consumer.class);
         mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener);
 
@@ -228,8 +232,43 @@
     }
 
     @Test
+    public void hideSilentNotificationsPerUserSettingWithHighPriorityParent() {
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+        mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+        mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
+        GroupEntry parent = new GroupEntryBuilder()
+                .setKey("parent")
+                .addChild(mEntry)
+                .setSummary(new NotificationEntryBuilder()
+                        .setUser(new UserHandle(NOTIF_USER_ID))
+                        .setImportance(IMPORTANCE_LOW)
+                        .build())
+                .build();
+        mEntry = new NotificationEntryBuilder()
+                .setUser(new UserHandle(NOTIF_USER_ID))
+                .setImportance(IMPORTANCE_LOW)
+                .setParent(parent)
+                .build();
+        when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false);
+        assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
+    }
+
+    @Test
+    public void hideSilentNotificationsPerUserSetting() {
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+        mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+        mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
+        mEntry = new NotificationEntryBuilder()
+                .setUser(new UserHandle(NOTIF_USER_ID))
+                .setImportance(IMPORTANCE_LOW)
+                .build();
+        when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false);
+        assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
+    }
+
+    @Test
     public void notifyListeners_onSettingChange_zenMode() {
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
         Consumer<String> listener = mock(Consumer.class);
         mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener);
 
@@ -240,7 +279,7 @@
 
     @Test
     public void notifyListeners_onSettingChange_lockScreenShowSilentNotifs() {
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
         Consumer<String> listener = mock(Consumer.class);
         mKeyguardNotificationVisibilityProvider.addOnStateChangedListener(listener);
 
@@ -262,7 +301,7 @@
     public void keyguardNotShowing() {
         // GIVEN the lockscreen isn't showing
         setupUnfilteredState(mEntry);
-        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(SHADE);
 
         // THEN don't filter out the entry
         assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
@@ -384,8 +423,8 @@
         mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
         when(mHighPriorityProvider.isHighPriority(parent)).thenReturn(true);
 
-        // THEN don't filter out the entry
-        assertFalse(
+        // THEN filter out the entry regardless of parent
+        assertTrue(
                 mKeyguardNotificationVisibilityProvider.shouldHideNotification(entryWithParent));
 
         // WHEN its parent doesn't exceed threshold to show on lockscreen
@@ -404,7 +443,7 @@
      */
     private void setupUnfilteredState(NotificationEntry entry) {
         // keyguard is showing
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
 
         // show notifications on the lockscreen
         when(mLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn(true);
@@ -452,11 +491,11 @@
                     @BindsInstance NotificationLockscreenUserManager lockscreenUserManager,
                     @BindsInstance KeyguardUpdateMonitor keyguardUpdateMonitor,
                     @BindsInstance HighPriorityProvider highPriorityProvider,
-                    @BindsInstance StatusBarStateController statusBarStateController,
+                    @BindsInstance SysuiStatusBarStateController statusBarStateController,
                     @BindsInstance BroadcastDispatcher broadcastDispatcher,
                     @BindsInstance SecureSettings secureSettings,
                     @BindsInstance GlobalSettings globalSettings
             );
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
index 4986792..f43c2a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
@@ -160,7 +160,7 @@
     @Test
     public void testShow_notifiesVisibility() {
         mBouncer.show(true);
-        verify(mViewMediatorCallback).onBouncerVisiblityChanged(eq(true));
+        verify(mKeyguardStateController).notifyBouncerShowing(eq(true));
         verify(mExpansionCallback).onStartingToShow();
 
         // Not called again when visible
@@ -238,7 +238,7 @@
     @Test
     public void testHide_notifiesVisibility() {
         mBouncer.hide(false);
-        verify(mViewMediatorCallback).onBouncerVisiblityChanged(eq(false));
+        verify(mKeyguardStateController).notifyBouncerShowing(eq(false));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
index ddccd83..c402d2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
@@ -26,10 +26,12 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.IActivityManager;
@@ -103,6 +105,7 @@
         mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
 
         mNotificationShadeWindowController.attach();
+        verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
     }
 
     @Test
@@ -174,6 +177,14 @@
     }
 
     @Test
+    public void setScrimsVisibility_earlyReturn() {
+        clearInvocations(mWindowManager);
+        mNotificationShadeWindowController.setScrimsVisibility(ScrimController.TRANSPARENT);
+        // Abort early if value didn't change
+        verify(mWindowManager, never()).updateViewLayout(any(), mLayoutParameters.capture());
+    }
+
+    @Test
     public void attach_animatingKeyguardAndSurface_wallpaperVisible() {
         clearInvocations(mWindowManager);
         when(mKeyguardViewMediator.isShowingAndNotOccluded()).thenReturn(true);
@@ -221,6 +232,8 @@
     public void setPanelExpanded_notFocusable_altFocusable_whenPanelIsOpen() {
         mNotificationShadeWindowController.setPanelExpanded(true);
         clearInvocations(mWindowManager);
+        mNotificationShadeWindowController.setPanelExpanded(true);
+        verifyNoMoreInteractions(mWindowManager);
         mNotificationShadeWindowController.setNotificationShadeFocusable(true);
 
         verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
@@ -287,6 +300,8 @@
     public void batchApplyWindowLayoutParams_doesNotDispatchEvents() {
         mNotificationShadeWindowController.setForceDozeBrightness(true);
         verify(mWindowManager).updateViewLayout(any(), any());
+        mNotificationShadeWindowController.setForceDozeBrightness(true);
+        verifyNoMoreInteractions(mWindowManager);
 
         clearInvocations(mWindowManager);
         mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
@@ -295,4 +310,17 @@
         });
         verify(mWindowManager).updateViewLayout(any(), any());
     }
+
+    @Test
+    public void bouncerShowing_OrientationNoSensor() {
+        mNotificationShadeWindowController.setKeyguardShowing(true);
+        mNotificationShadeWindowController.setKeyguardOccluded(true);
+        mNotificationShadeWindowController.setBouncerShowing(true);
+        when(mKeyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(false);
+        mNotificationShadeWindowController.onConfigChanged(new Configuration());
+
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        assertThat(mLayoutParameters.getValue().screenOrientation)
+                .isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index 0936b77..0112797 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
@@ -117,11 +117,18 @@
         val keyguardSpy = spy(keyguardView)
         Mockito.`when`(keyguardSpy.animate()).thenReturn(animator)
         val listener = ArgumentCaptor.forClass(Animator.AnimatorListener::class.java)
+        val endAction = ArgumentCaptor.forClass(Runnable::class.java)
         controller.animateInKeyguard(keyguardSpy, Runnable {})
         Mockito.verify(animator).setListener(listener.capture())
-        // Verify that the listener is cleared when it ends
-        listener.value.onAnimationEnd(null)
+        Mockito.verify(animator).withEndAction(endAction.capture())
+
+        // Verify that the listener is cleared if we cancel it.
+        listener.value.onAnimationCancel(null)
         Mockito.verify(animator).setListener(null)
+
+        // Verify that the listener is also cleared if the end action is triggered.
+        endAction.value.run()
+        verify(animator, times(2)).setListener(null)
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
index e9d16a6..64a93cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
@@ -53,7 +53,6 @@
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
 import org.mockito.Mockito.never
-import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.ArgumentMatchers.anyInt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java
index 14e0878..9c7dc6b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java
@@ -365,46 +365,4 @@
         // No new callbacks
         verify(callback).onLocationSettingsChanged(anyBoolean());
     }
-
-    @Test
-    public void testExperimentFlipsSystemFlag() throws Exception {
-        mSecureSettings.putInt(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0);
-        mDeviceConfigProxy.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SMALL_ENABLED,
-                "true",
-                true);
-        // Show system experiment not running
-        mDeviceConfigProxy.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM,
-                "false",
-                false);
-        mTestableLooper.processAllMessages();
-
-        // Flip experiment on
-        mDeviceConfigProxy.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM,
-                "true",
-                true);
-        mTestableLooper.processAllMessages();
-
-        // Verify settings were flipped
-        assertThat(mSecureSettings.getInt(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS))
-                .isEqualTo(1);
-        assertThat(mSecureSettings.getInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED))
-                .isEqualTo(1);
-
-        // Flip experiment off
-        mDeviceConfigProxy.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM,
-                "false",
-                false);
-        mTestableLooper.processAllMessages();
-
-        assertThat(mSecureSettings.getInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED))
-                .isEqualTo(0);
-    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
index aaea4ec..95b62a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
@@ -49,6 +49,11 @@
     }
 
     @Override
+    public boolean isBouncerShowing() {
+        return false;
+    }
+
+    @Override
     public boolean canDismissLockScreen() {
         return false;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 193879e..238a4d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -57,6 +57,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
@@ -70,6 +71,8 @@
 import android.service.notification.ZenModeConfig;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.util.Pair;
+import android.util.SparseArray;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -147,6 +150,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
 
@@ -1010,7 +1014,7 @@
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
 
         // Should notify delegate that shade state changed
-        verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+        verify(mBubbleController).onBubbleMetadataFlagChanged(
                 mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
     }
 
@@ -1027,7 +1031,7 @@
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
 
         // Should notify delegate that shade state changed
-        verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+        verify(mBubbleController).onBubbleMetadataFlagChanged(
                 mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
     }
 
@@ -1447,6 +1451,69 @@
         assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
+    @Test
+    public void testSetShouldAutoExpand_notifiesFlagChanged() {
+        mEntryListener.onPendingEntryAdded(mRow);
+
+        assertTrue(mBubbleController.hasBubbles());
+        Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey());
+        assertThat(b.shouldAutoExpand()).isFalse();
+
+        // Set it to the same thing
+        b.setShouldAutoExpand(false);
+
+        // Verify it doesn't notify
+        verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any());
+
+        // Set it to something different
+        b.setShouldAutoExpand(true);
+        verify(mBubbleController).onBubbleMetadataFlagChanged(b);
+    }
+
+    @Test
+    public void testUpdateBubble_skipsDndSuppressListNotifs() {
+        mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+                mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+                mRow.shouldSuppressPeek());
+        mBubbleEntry.getBubbleMetadata().setFlags(
+                Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+
+        mBubbleController.updateBubble(mBubbleEntry);
+
+        Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey());
+        assertThat(b.shouldAutoExpand()).isFalse();
+        assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull();
+    }
+
+    @Test
+    public void testOnRankingUpdate_DndSuppressListNotif() {
+        // It's in the stack
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue();
+
+        // Set current user profile
+        SparseArray<UserInfo> userInfos = new SparseArray<>();
+        userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(),
+                mock(UserInfo.class));
+        mBubbleController.onCurrentProfilesChanged(userInfos);
+
+        // Send ranking update that the notif is suppressed from the list.
+        HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>();
+        mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+                mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+                mRow.shouldSuppressPeek());
+        Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true);
+        entryDataByKey.put(mBubbleEntry.getKey(), pair);
+
+        NotificationListenerService.RankingMap rankingMap =
+                mock(NotificationListenerService.RankingMap.class);
+        when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() });
+        mBubbleController.onRankingUpdated(rankingMap, entryDataByKey);
+
+        // Should no longer be in the stack
+        assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse();
+    }
+
     /** Creates a bubble using the userId and package. */
     private Bubble createBubble(int userId, String pkg) {
         final UserHandle userHandle = new UserHandle(userId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index 02d8691..dff89e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -50,6 +50,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.LauncherApps;
+import android.content.pm.UserInfo;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Handler;
 import android.os.PowerManager;
@@ -59,6 +60,8 @@
 import android.service.notification.ZenModeConfig;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.util.Pair;
+import android.util.SparseArray;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -128,6 +131,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
 
@@ -880,7 +884,7 @@
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
 
         // Should notify delegate that shade state changed
-        verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+        verify(mBubbleController).onBubbleMetadataFlagChanged(
                 mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
     }
 
@@ -897,7 +901,7 @@
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
 
         // Should notify delegate that shade state changed
-        verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+        verify(mBubbleController).onBubbleMetadataFlagChanged(
                 mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
     }
 
@@ -1267,6 +1271,69 @@
         assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
+    @Test
+    public void testSetShouldAutoExpand_notifiesFlagChanged() {
+        mBubbleController.updateBubble(mBubbleEntry);
+
+        assertTrue(mBubbleController.hasBubbles());
+        Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey());
+        assertThat(b.shouldAutoExpand()).isFalse();
+
+        // Set it to the same thing
+        b.setShouldAutoExpand(false);
+
+        // Verify it doesn't notify
+        verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any());
+
+        // Set it to something different
+        b.setShouldAutoExpand(true);
+        verify(mBubbleController).onBubbleMetadataFlagChanged(b);
+    }
+
+    @Test
+    public void testUpdateBubble_skipsDndSuppressListNotifs() {
+        mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+                mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+                mRow.shouldSuppressPeek());
+        mBubbleEntry.getBubbleMetadata().setFlags(
+                Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+
+        mBubbleController.updateBubble(mBubbleEntry);
+
+        Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey());
+        assertThat(b.shouldAutoExpand()).isFalse();
+        assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull();
+    }
+
+    @Test
+    public void testOnRankingUpdate_DndSuppressListNotif() {
+        // It's in the stack
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue();
+
+        // Set current user profile
+        SparseArray<UserInfo> userInfos = new SparseArray<>();
+        userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(),
+                mock(UserInfo.class));
+        mBubbleController.onCurrentProfilesChanged(userInfos);
+
+        // Send ranking update that the notif is suppressed from the list.
+        HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>();
+        mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+                mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+                mRow.shouldSuppressPeek());
+        Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true);
+        entryDataByKey.put(mBubbleEntry.getKey(), pair);
+
+        NotificationListenerService.RankingMap rankingMap =
+                mock(NotificationListenerService.RankingMap.class);
+        when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() });
+        mBubbleController.onRankingUpdated(rankingMap, entryDataByKey);
+
+        // Should no longer be in the stack
+        assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse();
+    }
+
     /**
      * Sets the bubble metadata flags for this entry. These flags are normally set by
      * NotificationManagerService when the notification is sent, however, these tests do not
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index b4c107c..62bb9f1 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -294,6 +294,15 @@
 
     private boolean onCompanionApplicationBindingDiedInternal(
             @UserIdInt int userId, @NonNull String packageName) {
+        // Update the current connected devices sets when binderDied, so that application is able
+        // to call notifyDeviceAppeared after re-launch the application.
+        for (AssociationInfo ai :
+                mAssociationStore.getAssociationsForPackage(userId, packageName)) {
+            int id = ai.getId();
+            Slog.i(TAG, "Removing association id: " + id + " for package: "
+                    + packageName + " due to binderDied.");
+            mDevicePresenceMonitor.removeDeviceFromMonitoring(id);
+        }
         // TODO(b/218613015): implement.
         return false;
     }
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index 6371b25..24be1b6 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -206,6 +206,15 @@
     }
 
     /**
+     * Remove the current connected devices by associationId.
+     */
+    public void removeDeviceFromMonitoring(int associationId) {
+        mConnectedBtDevices.remove(associationId);
+        mNearbyBleDevices.remove(associationId);
+        mReportedSelfManagedDevices.remove(associationId);
+    }
+
+    /**
      * Implements
      * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
      */
@@ -217,9 +226,7 @@
             Log.d(TAG, "  > association=" + association);
         }
 
-        mConnectedBtDevices.remove(id);
-        mNearbyBleDevices.remove(id);
-        mReportedSelfManagedDevices.remove(id);
+        removeDeviceFromMonitoring(id);
 
         // Do NOT call mCallback.onDeviceDisappeared()!
         // CompanionDeviceManagerService will know that the association is removed, and will do
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 9d4b50b..80182d2 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -22,6 +22,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.InputManager;
 import android.hardware.input.InputManagerInternal;
 import android.hardware.input.VirtualKeyEvent;
@@ -29,11 +30,13 @@
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualTouchEvent;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.Display;
+import android.view.InputDevice;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -44,7 +47,11 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
 
 /** Controls virtual input devices, including device lifecycle and event dispatch. */
 class InputController {
@@ -72,20 +79,27 @@
     @GuardedBy("mLock")
     final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
 
+    private final Handler mHandler;
     private final NativeWrapper mNativeWrapper;
     private final DisplayManagerInternal mDisplayManagerInternal;
     private final InputManagerInternal mInputManagerInternal;
+    private final DeviceCreationThreadVerifier mThreadVerifier;
 
-    InputController(@NonNull Object lock) {
-        this(lock, new NativeWrapper());
+    InputController(@NonNull Object lock, @NonNull Handler handler) {
+        this(lock, new NativeWrapper(), handler,
+                // Verify that virtual devices are not created on the handler thread.
+                () -> !handler.getLooper().isCurrentThread());
     }
 
     @VisibleForTesting
-    InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper) {
+    InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper,
+            @NonNull Handler handler, @NonNull DeviceCreationThreadVerifier threadVerifier) {
         mLock = lock;
+        mHandler = handler;
         mNativeWrapper = nativeWrapper;
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+        mThreadVerifier = threadVerifier;
     }
 
     void close() {
@@ -108,23 +122,13 @@
             @NonNull IBinder deviceToken,
             int displayId) {
         final String phys = createPhys(PHYS_TYPE_KEYBOARD);
-        setUniqueIdAssociation(displayId, phys);
-        final int fd = mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys);
-        if (fd < 0) {
-            throw new RuntimeException(
-                    "A native error occurred when creating keyboard: " + -fd);
-        }
-        final BinderDeathRecipient binderDeathRecipient = new BinderDeathRecipient(deviceToken);
-        synchronized (mLock) {
-            mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, binderDeathRecipient,
-                            InputDeviceDescriptor.TYPE_KEYBOARD, displayId, phys));
-        }
         try {
-            deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
-        } catch (RemoteException e) {
-            // TODO(b/215608394): remove and close InputDeviceDescriptor
-            throw new RuntimeException("Could not create virtual keyboard", e);
+            createDeviceInternal(InputDeviceDescriptor.TYPE_KEYBOARD, deviceName, vendorId,
+                    productId, deviceToken, displayId, phys,
+                    () -> mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys));
+        } catch (DeviceCreationException e) {
+            throw new RuntimeException(
+                    "Failed to create virtual keyboard device '" + deviceName + "'.", e);
         }
     }
 
@@ -134,25 +138,15 @@
             @NonNull IBinder deviceToken,
             int displayId) {
         final String phys = createPhys(PHYS_TYPE_MOUSE);
-        setUniqueIdAssociation(displayId, phys);
-        final int fd = mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys);
-        if (fd < 0) {
-            throw new RuntimeException(
-                    "A native error occurred when creating mouse: " + -fd);
-        }
-        final BinderDeathRecipient binderDeathRecipient = new BinderDeathRecipient(deviceToken);
-        synchronized (mLock) {
-            mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, binderDeathRecipient,
-                            InputDeviceDescriptor.TYPE_MOUSE, displayId, phys));
-            mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
-        }
         try {
-            deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
-        } catch (RemoteException e) {
-            // TODO(b/215608394): remove and close InputDeviceDescriptor
-            throw new RuntimeException("Could not create virtual mouse", e);
+            createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
+                    deviceToken, displayId, phys,
+                    () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
+        } catch (DeviceCreationException e) {
+            throw new RuntimeException(
+                    "Failed to create virtual mouse device: '" + deviceName + "'.", e);
         }
+        mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
     }
 
     void createTouchscreen(@NonNull String deviceName,
@@ -162,24 +156,14 @@
             int displayId,
             @NonNull Point screenSize) {
         final String phys = createPhys(PHYS_TYPE_TOUCHSCREEN);
-        setUniqueIdAssociation(displayId, phys);
-        final int fd = mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId, phys,
-                screenSize.y, screenSize.x);
-        if (fd < 0) {
-            throw new RuntimeException(
-                    "A native error occurred when creating touchscreen: " + -fd);
-        }
-        final BinderDeathRecipient binderDeathRecipient = new BinderDeathRecipient(deviceToken);
-        synchronized (mLock) {
-            mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, binderDeathRecipient,
-                            InputDeviceDescriptor.TYPE_TOUCHSCREEN, displayId, phys));
-        }
         try {
-            deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
-        } catch (RemoteException e) {
-            // TODO(b/215608394): remove and close InputDeviceDescriptor
-            throw new RuntimeException("Could not create virtual touchscreen", e);
+            createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
+                    productId, deviceToken, displayId, phys,
+                    () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
+                            phys, screenSize.y, screenSize.x));
+        } catch (DeviceCreationException e) {
+            throw new RuntimeException(
+                    "Failed to create virtual touchscreen device '" + deviceName + "'.", e);
         }
     }
 
@@ -510,4 +494,133 @@
             unregisterInputDevice(mDeviceToken);
         }
     }
+
+    /** A helper class used to wait for an input device to be registered. */
+    private class WaitForDevice implements AutoCloseable {
+        private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1);
+        private final InputManager.InputDeviceListener mListener;
+
+        WaitForDevice(String deviceName, int vendorId, int productId) {
+            mListener = new InputManager.InputDeviceListener() {
+                @Override
+                public void onInputDeviceAdded(int deviceId) {
+                    final InputDevice device = InputManager.getInstance().getInputDevice(
+                            deviceId);
+                    Objects.requireNonNull(device, "Newly added input device was null.");
+                    if (!device.getName().equals(deviceName)) {
+                        return;
+                    }
+                    final InputDeviceIdentifier id = device.getIdentifier();
+                    if (id.getVendorId() != vendorId || id.getProductId() != productId) {
+                        return;
+                    }
+                    mDeviceAddedLatch.countDown();
+                }
+
+                @Override
+                public void onInputDeviceRemoved(int deviceId) {
+
+                }
+
+                @Override
+                public void onInputDeviceChanged(int deviceId) {
+
+                }
+            };
+            InputManager.getInstance().registerInputDeviceListener(mListener, mHandler);
+        }
+
+        /** Note: This must not be called from {@link #mHandler}'s thread. */
+        void waitForDeviceCreation() throws DeviceCreationException {
+            try {
+                if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) {
+                    throw new DeviceCreationException(
+                            "Timed out waiting for virtual device to be created.");
+                }
+            } catch (InterruptedException e) {
+                throw new DeviceCreationException(
+                        "Interrupted while waiting for virtual device to be created.", e);
+            }
+        }
+
+        @Override
+        public void close() {
+            InputManager.getInstance().unregisterInputDeviceListener(mListener);
+        }
+    }
+
+    /** An internal exception that is thrown to indicate an error when opening a virtual device. */
+    private static class DeviceCreationException extends Exception {
+        DeviceCreationException(String message) {
+            super(message);
+        }
+        DeviceCreationException(String message, Exception cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     * Creates a virtual input device synchronously, and waits for the notification that the device
+     * was added.
+     *
+     * Note: Input device creation is expected to happen on a binder thread, and the calling thread
+     * will be blocked until the input device creation is successful. This should not be called on
+     * the handler's thread.
+     *
+     * @throws DeviceCreationException Throws this exception if anything unexpected happens in the
+     *                                 process of creating the device. This method will take care
+     *                                 to restore the state of the system in the event of any
+     *                                 unexpected behavior.
+     */
+    private void createDeviceInternal(@InputDeviceDescriptor.Type int type, String deviceName,
+            int vendorId, int productId, IBinder deviceToken, int displayId, String phys,
+            Supplier<Integer> deviceOpener)
+            throws DeviceCreationException {
+        if (!mThreadVerifier.isValidThread()) {
+            throw new IllegalStateException(
+                    "Virtual device creation should happen on an auxiliary thread (e.g. binder "
+                            + "thread) and not from the handler's thread.");
+        }
+
+        final int fd;
+        final BinderDeathRecipient binderDeathRecipient;
+
+        setUniqueIdAssociation(displayId, phys);
+        try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
+            fd = deviceOpener.get();
+            if (fd < 0) {
+                throw new DeviceCreationException(
+                        "A native error occurred when creating touchscreen: " + -fd);
+            }
+            // The fd is valid from here, so ensure that all failures close the fd after this point.
+            try {
+                waiter.waitForDeviceCreation();
+
+                binderDeathRecipient = new BinderDeathRecipient(deviceToken);
+                try {
+                    deviceToken.linkToDeath(binderDeathRecipient, /* flags= */ 0);
+                } catch (RemoteException e) {
+                    throw new DeviceCreationException(
+                            "Client died before virtual device could be created.", e);
+                }
+            } catch (DeviceCreationException e) {
+                mNativeWrapper.closeUinput(fd);
+                throw e;
+            }
+        } catch (DeviceCreationException e) {
+            InputManager.getInstance().removeUniqueIdAssociation(phys);
+            throw e;
+        }
+
+        synchronized (mLock) {
+            mInputDeviceDescriptors.put(deviceToken,
+                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys));
+        }
+    }
+
+    @VisibleForTesting
+    interface DeviceCreationThreadVerifier {
+        /** Returns true if the calling thread is a valid thread for device creation. */
+        boolean isValidThread();
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index de14ef6..9802b97 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -166,7 +166,8 @@
         mAppToken = token;
         mParams = params;
         if (inputController == null) {
-            mInputController = new InputController(mVirtualDeviceLock);
+            mInputController = new InputController(
+                    mVirtualDeviceLock, context.getMainThreadHandler());
         } else {
             mInputController = inputController;
         }
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 8f37823..bc40170 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3055,19 +3055,7 @@
             return true;
         }
 
-        if (packageName == null) {
-            return false;
-        }
-
-        final int packageUid = mPmInternal.getPackageUid(packageName,
-                PackageManager.MATCH_DEBUG_TRIAGED_MISSING, UserHandle.getUserId(callerUid));
-
-        if (DEBUG_OBB) {
-            Slog.d(TAG, "packageName = " + packageName + ", packageUid = " +
-                    packageUid + ", callerUid = " + callerUid);
-        }
-
-        return callerUid == packageUid;
+        return mPmInternal.isSameApp(packageName, callerUid, UserHandle.getUserId(callerUid));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2ceb00d..dea8cc0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -89,7 +89,6 @@
 import static android.os.Process.myPid;
 import static android.os.Process.myUid;
 import static android.os.Process.readProcFile;
-import static android.os.Process.removeAllProcessGroups;
 import static android.os.Process.sendSignal;
 import static android.os.Process.setThreadPriority;
 import static android.os.Process.setThreadScheduler;
@@ -2440,8 +2439,6 @@
     }
 
     private void start() {
-        removeAllProcessGroups();
-
         mBatteryStatsService.publish();
         mAppOpsService.publish();
         mProcessStats.publish();
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index f7abb11..6f5d87b 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -265,6 +265,20 @@
      */
     private int[] mDeviceIdleExceptIdleAllowlist = new int[0]; // No lock is needed.
 
+    /**
+     * The pre-configured system app-ids in the power-save allow list.
+     *
+     * @see #mDeviceIdleAllowlist.
+     */
+    private final ArraySet<Integer> mSystemDeviceIdleAllowlist = new ArraySet<>();
+
+    /**
+     * The pre-configured system app-ids in the power-save allow list, except-idle.
+     *
+     * @see #mDeviceIdleExceptIdleAllowlist.
+     */
+    private final ArraySet<Integer> mSystemDeviceIdleExceptIdleAllowlist = new ArraySet<>();
+
     private final Object mLock = new Object();
     private final Object mSettingsLock = new Object();
     private final Injector mInjector;
@@ -1511,14 +1525,33 @@
     }
 
     private void initBgRestrictionExemptioFromSysConfig() {
-        mBgRestrictionExemptioFromSysConfig =
-                SystemConfig.getInstance().getBgRestrictionExemption();
+        final SystemConfig sysConfig = SystemConfig.getInstance();
+        mBgRestrictionExemptioFromSysConfig = sysConfig.getBgRestrictionExemption();
         if (DEBUG_BG_RESTRICTION_CONTROLLER) {
             final ArraySet<String> exemptedPkgs = mBgRestrictionExemptioFromSysConfig;
             for (int i = exemptedPkgs.size() - 1; i >= 0; i--) {
                 Slog.i(TAG, "bg-restriction-exemption: " + exemptedPkgs.valueAt(i));
             }
         }
+        loadAppIdsFromPackageList(sysConfig.getAllowInPowerSaveExceptIdle(),
+                mSystemDeviceIdleExceptIdleAllowlist);
+        loadAppIdsFromPackageList(sysConfig.getAllowInPowerSave(), mSystemDeviceIdleAllowlist);
+    }
+
+    private void loadAppIdsFromPackageList(ArraySet<String> packages, ArraySet<Integer> apps) {
+        final PackageManager pm = mInjector.getPackageManager();
+        for (int i = packages.size() - 1; i >= 0; i--) {
+            final String pkg = packages.valueAt(i);
+            try {
+                final ApplicationInfo ai = pm.getApplicationInfo(pkg,
+                        PackageManager.MATCH_SYSTEM_ONLY);
+                if (ai == null) {
+                    continue;
+                }
+                apps.add(UserHandle.getAppId(ai.uid));
+            } catch (PackageManager.NameNotFoundException e) {
+            }
+        }
     }
 
     private boolean isExemptedFromSysConfig(String packageName) {
@@ -2685,6 +2718,13 @@
                 || Arrays.binarySearch(mDeviceIdleExceptIdleAllowlist, appId) >= 0;
     }
 
+    boolean isOnSystemDeviceIdleAllowlist(int uid) {
+        final int appId = UserHandle.getAppId(uid);
+
+        return mSystemDeviceIdleAllowlist.contains(appId)
+                || mSystemDeviceIdleExceptIdleAllowlist.contains(appId);
+    }
+
     void setDeviceIdleAllowlist(int[] allAppids, int[] exceptIdleAppids) {
         mDeviceIdleAllowlist = allAppids;
         mDeviceIdleExceptIdleAllowlist = exceptIdleAppids;
@@ -2703,6 +2743,9 @@
         if (UserHandle.isCore(uid)) {
             return REASON_SYSTEM_UID;
         }
+        if (isOnSystemDeviceIdleAllowlist(uid)) {
+            return REASON_SYSTEM_ALLOW_LISTED;
+        }
         if (isOnDeviceIdleAllowlist(uid)) {
             return REASON_ALLOWLISTED_PACKAGE;
         }
@@ -2748,7 +2791,7 @@
                 } else if (isExemptedFromSysConfig(pkg)) {
                     return REASON_SYSTEM_ALLOW_LISTED;
                 } else if (mConstantsObserver.mBgRestrictionExemptedPackages.contains(pkg)) {
-                    return REASON_ALLOWLISTED_PACKAGE;
+                    return REASON_SYSTEM_ALLOW_LISTED;
                 } else if (pm.isPackageStateProtected(pkg, userId)) {
                     return REASON_DPO_PROTECTED_APP;
                 }
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index a172018..e49497e 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -479,19 +479,33 @@
     @GuardedBy("mProcLock")
     void compactAppSome(ProcessRecord app, boolean force) {
         app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_SOME);
-        if (DEBUG_COMPACTION) {
-            Slog.d(TAG_AM, " compactAppSome requested for " + app.processName + " force: " + force);
+        compactApp(app, force, "some");
+    }
+
+    // This method returns true only if requirements are met. Note, that requirements are different
+    // from throttles applied at the time a compaction is trying to be executed in the sense that
+    // these are not subject to change dependent on time or memory as throttles usually do.
+    @GuardedBy("mProcLock")
+    boolean meetsCompactionRequirements(ProcessRecord proc) {
+        if (mAm.mInternal.isPendingTopUid(proc.uid)) {
+            // In case the OOM Adjust has not yet been propagated we see if this is
+            // pending on becoming top app in which case we should not compact.
+            if (DEBUG_COMPACTION) {
+                Slog.d(TAG_AM, "Skip compaction since UID is active for  " + proc.processName);
+            }
+            return false;
         }
-        if (force || !app.mOptRecord.hasPendingCompact()) {
-            Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK,
-                    "compactAppSome " + app.processName != null ? app.processName : "");
-            app.mOptRecord.setHasPendingCompact(true);
-            app.mOptRecord.setForceCompact(force);
-            mPendingCompactionProcesses.add(app);
-            mCompactionHandler.sendMessage(
-                    mCompactionHandler.obtainMessage(
-                    COMPACT_PROCESS_MSG, app.mState.getSetAdj(), app.mState.getSetProcState()));
+
+        if (proc.mState.hasForegroundActivities()) {
+            if (DEBUG_COMPACTION) {
+                Slog.e(TAG_AM,
+                        "Skip compaction as process " + proc.processName
+                                + " has foreground activities");
+            }
+            return false;
         }
+
+        return true;
     }
 
     @GuardedBy("mProcLock")
@@ -508,19 +522,7 @@
         // Apply OOM adj score throttle for Full App Compaction.
         if (force || oomAdjEnteredCached) {
             app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_FULL);
-            if (!app.mOptRecord.hasPendingCompact()) {
-                Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK,
-                        "compactAppFull " + app.processName != null ? app.processName : "");
-                app.mOptRecord.setHasPendingCompact(true);
-                app.mOptRecord.setForceCompact(force);
-                mPendingCompactionProcesses.add(app);
-                mCompactionHandler.sendMessage(mCompactionHandler.obtainMessage(
-                        COMPACT_PROCESS_MSG, app.mState.getSetAdj(), app.mState.getSetProcState()));
-            } else if (DEBUG_COMPACTION) {
-                Slog.d(TAG_AM,
-                        " compactAppFull Skipped for " + app.processName
-                                + " since it has a pending compact");
-            }
+            compactApp(app, force, "Full");
         } else {
             if (DEBUG_COMPACTION) {
                 Slog.d(TAG_AM, "Skipping full compaction for " + app.processName
@@ -533,15 +535,34 @@
     @GuardedBy("mProcLock")
     void compactAppPersistent(ProcessRecord app) {
         app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_PERSISTENT);
-        if (!app.mOptRecord.hasPendingCompact()) {
+        compactApp(app, false, "Persistent");
+    }
+
+    @GuardedBy("mProcLock")
+    boolean compactApp(ProcessRecord app, boolean force, String compactRequestType) {
+        if (!app.mOptRecord.hasPendingCompact() && meetsCompactionRequirements(app)) {
+            final String processName = (app.processName != null ? app.processName : "");
+            if (DEBUG_COMPACTION) {
+                Slog.d(TAG_AM, "compactApp " + compactRequestType + " " + processName);
+            }
             Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK,
-                    "compactAppPersistent " + app.processName != null ? app.processName : "");
+                    "compactApp " + compactRequestType + " " + processName);
             app.mOptRecord.setHasPendingCompact(true);
+            app.mOptRecord.setForceCompact(force);
             mPendingCompactionProcesses.add(app);
-            mCompactionHandler.sendMessage(
-                    mCompactionHandler.obtainMessage(
+            mCompactionHandler.sendMessage(mCompactionHandler.obtainMessage(
                     COMPACT_PROCESS_MSG, app.mState.getCurAdj(), app.mState.getSetProcState()));
+            return true;
         }
+
+        if (DEBUG_COMPACTION) {
+            Slog.d(TAG_AM,
+                    " compactApp Skipped for " + app.processName
+                            + " pendingCompact= " + app.mOptRecord.hasPendingCompact()
+                            + " meetsCompactionRequirements=" + meetsCompactionRequirements(app)
+                            + ". Requested compact: " + app.mOptRecord.getReqCompactAction());
+        }
+        return false;
     }
 
     @GuardedBy("mProcLock")
@@ -553,15 +574,7 @@
     @GuardedBy("mProcLock")
     void compactAppBfgs(ProcessRecord app) {
         app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_BFGS);
-        if (!app.mOptRecord.hasPendingCompact()) {
-            Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK,
-                    "compactAppBfgs " + app.processName != null ? app.processName : "");
-            app.mOptRecord.setHasPendingCompact(true);
-            mPendingCompactionProcesses.add(app);
-            mCompactionHandler.sendMessage(
-                    mCompactionHandler.obtainMessage(
-                    COMPACT_PROCESS_MSG, app.mState.getCurAdj(), app.mState.getSetProcState()));
-        }
+        compactApp(app, false, " Bfgs");
     }
 
     @GuardedBy("mProcLock")
@@ -572,6 +585,9 @@
 
     void compactAllSystem() {
         if (useCompaction()) {
+            if (DEBUG_COMPACTION) {
+                Slog.d(TAG_AM, "compactAllSystem");
+            }
             Trace.instantForTrack(
                     Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK, "compactAllSystem");
             mCompactionHandler.sendMessage(mCompactionHandler.obtainMessage(
@@ -1175,13 +1191,13 @@
             cancelCompaction();
         }
 
-        // Perform a minor compaction when a perceptible app becomes the prev/home app
-        // Perform a major compaction when any app enters cached
         if (oldAdj <= ProcessList.PERCEPTIBLE_APP_ADJ
                 && (newAdj == ProcessList.PREVIOUS_APP_ADJ || newAdj == ProcessList.HOME_APP_ADJ)) {
+            // Perform a minor compaction when a perceptible app becomes the prev/home app
             compactAppSome(app, false);
         } else if (newAdj >= ProcessList.CACHED_APP_MIN_ADJ
                 && newAdj <= ProcessList.CACHED_APP_MAX_ADJ) {
+            // Perform a major compaction when any app enters cached
             compactAppFull(app, false);
         }
     }
@@ -1241,12 +1257,6 @@
 
         private boolean shouldOomAdjThrottleCompaction(ProcessRecord proc, int action) {
             final String name = proc.processName;
-            if (mAm.mInternal.isPendingTopUid(proc.uid)) {
-                // In case the OOM Adjust has not yet been propagated we see if this is
-                // pending on becoming top app in which case we should not compact.
-                Slog.e(TAG_AM, "Skip compaction since UID is active for  " + name);
-                return true;
-            }
 
             // don't compact if the process has returned to perceptible
             // and this is only a cached/home/prev compaction
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index b6757c8..17fff91 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -72,7 +72,7 @@
             Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
             Context.BIND_FOREGROUND_SERVICE,
             Context.BIND_TREAT_LIKE_ACTIVITY,
-            Context.BIND_VISIBLE,
+            Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
             Context.BIND_SHOWING_UI,
             Context.BIND_NOT_VISIBLE,
             Context.BIND_NOT_PERCEPTIBLE,
@@ -225,8 +225,8 @@
         if ((flags & Context.BIND_SCHEDULE_LIKE_TOP_APP) != 0) {
             sb.append("SLTA ");
         }
-        if ((flags&Context.BIND_VISIBLE) != 0) {
-            sb.append("VIS ");
+        if ((flags & Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE) != 0) {
+            sb.append("VFGS ");
         }
         if ((flags&Context.BIND_SHOWING_UI) != 0) {
             sb.append("UI ");
diff --git a/services/core/java/com/android/server/am/ErrorDialogController.java b/services/core/java/com/android/server/am/ErrorDialogController.java
index a4e8f92..82f35ad 100644
--- a/services/core/java/com/android/server/am/ErrorDialogController.java
+++ b/services/core/java/com/android/server/am/ErrorDialogController.java
@@ -144,7 +144,8 @@
         if (mWaitDialog == null) {
             return;
         }
-        mWaitDialog.dismiss();
+        final BaseErrorDialog dialog = mWaitDialog;
+        mService.mUiHandler.post(dialog::dismiss);
         mWaitDialog = null;
     }
 
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 4d10574..e7fcc59 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -39,6 +39,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
 import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
+import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
@@ -2061,6 +2062,10 @@
                                     newAdj = ProcessList.PERCEPTIBLE_APP_ADJ;
                                 } else if (clientAdj >= ProcessList.PERCEPTIBLE_APP_ADJ) {
                                     newAdj = clientAdj;
+                                } else if (cr.hasFlag(BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE)
+                                        && clientAdj <= ProcessList.VISIBLE_APP_ADJ
+                                        && adj > ProcessList.VISIBLE_APP_ADJ) {
+                                    newAdj = ProcessList.VISIBLE_APP_ADJ;
                                 } else {
                                     if (adj > ProcessList.VISIBLE_APP_ADJ) {
                                         // TODO: Is this too limiting for apps bound from TOP?
@@ -2097,7 +2102,9 @@
                                 // processes).  These should not bring the current process
                                 // into the top state, since they are not on top.  Instead
                                 // give them the best bound state after that.
-                                if (cr.hasFlag(Context.BIND_FOREGROUND_SERVICE)) {
+                                if (cr.hasFlag(BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE)) {
+                                    clientProcState = PROCESS_STATE_FOREGROUND_SERVICE;
+                                } else if (cr.hasFlag(Context.BIND_FOREGROUND_SERVICE)) {
                                     clientProcState = PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
                                 } else if (mService.mWakefulness.get()
                                         == PowerManagerInternal.WAKEFULNESS_AWAKE
@@ -2556,20 +2563,21 @@
             // reminder: here, setAdj is previous state, curAdj is upcoming state
             if (state.getCurAdj() != state.getSetAdj()) {
                 mCachedAppOptimizer.onOomAdjustChanged(state.getSetAdj(), state.getCurAdj(), app);
-            } else if (mService.mWakefulness.get() != PowerManagerInternal.WAKEFULNESS_AWAKE
-                    && state.getSetAdj() < ProcessList.FOREGROUND_APP_ADJ
-                    && !state.isRunningRemoteAnimation()
-                    // Because these can fire independent of oom_adj/procstate changes, we need
-                    // to throttle the actual dispatch of these requests in addition to the
-                    // processing of the requests. As a result, there is throttling both here
-                    // and in CachedAppOptimizer.
-                    && mCachedAppOptimizer.shouldCompactPersistent(app, now)) {
-                mCachedAppOptimizer.compactAppPersistent(app);
-            } else if (mService.mWakefulness.get() != PowerManagerInternal.WAKEFULNESS_AWAKE
-                    && state.getCurProcState()
-                        == ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
-                    && mCachedAppOptimizer.shouldCompactBFGS(app, now)) {
-                mCachedAppOptimizer.compactAppBfgs(app);
+            } else if (mService.mWakefulness.get() != PowerManagerInternal.WAKEFULNESS_AWAKE) {
+                // See if we can compact persistent and bfgs services now that screen is off
+                if (state.getSetAdj() < ProcessList.FOREGROUND_APP_ADJ
+                        && !state.isRunningRemoteAnimation()
+                        // Because these can fire independent of oom_adj/procstate changes, we need
+                        // to throttle the actual dispatch of these requests in addition to the
+                        // processing of the requests. As a result, there is throttling both here
+                        // and in CachedAppOptimizer.
+                        && mCachedAppOptimizer.shouldCompactPersistent(app, now)) {
+                    mCachedAppOptimizer.compactAppPersistent(app);
+                } else if (state.getCurProcState()
+                                == ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+                        && mCachedAppOptimizer.shouldCompactBFGS(app, now)) {
+                    mCachedAppOptimizer.compactAppBfgs(app);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1f3fbff..aed63ce 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -9135,6 +9135,8 @@
         if (timeOutMs <= 0 || usages.length == 0) {
             throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute");
         }
+        Log.i(TAG, "muteAwaitConnection dev:" + device + " timeOutMs:" + timeOutMs
+                + " usages:" + usages);
 
         if (mDeviceBroker.isDeviceConnected(device)) {
             // not throwing an exception as there could be a race between a connection (server-side,
@@ -9178,7 +9180,7 @@
                 Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device");
                 return;
             }
-            if (!device.equals(mMutingExpectedDevice)) {
+            if (!device.equalTypeAddress(mMutingExpectedDevice)) {
                 Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device
                         + "] but expected device is" + mMutingExpectedDevice);
                 throw new IllegalStateException("cancelMuteAwaitConnection for wrong device");
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 74c8999..565783f 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -1158,6 +1158,8 @@
     //==========================================================================================
     void muteAwaitConnection(@NonNull int[] usagesToMute,
             @NonNull AudioDeviceAttributes dev, long timeOutMs) {
+        sEventLogger.loglogi(
+                "muteAwaitConnection() dev:" + dev + " timeOutMs:" + timeOutMs, TAG);
         synchronized (mPlayerLock) {
             mutePlayersExpectingDevice(usagesToMute);
             // schedule timeout (remove previously scheduled first)
@@ -1169,6 +1171,7 @@
     }
 
     void cancelMuteAwaitConnection() {
+        sEventLogger.loglogi("cancelMuteAwaitConnection()", TAG);
         synchronized (mPlayerLock) {
             // cancel scheduled timeout, ignore device, only one expected device at a time
             mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION);
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index b5c8cd1..bc550d3 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -805,9 +805,10 @@
         if (isUdfps && udfpsProps.length == 3) {
             return new FingerprintSensorPropertiesInternal(sensorId,
                     Utils.authenticatorStrengthToPropertyStrength(strength), maxEnrollmentsPerUser,
-                    componentInfo, sensorType, resetLockoutRequiresHardwareAuthToken,
-                    List.of(new SensorLocationInternal("" /* display */,
-                            udfpsProps[0], udfpsProps[1], udfpsProps[2])));
+                    componentInfo, sensorType, true /* halControlsIllumination */,
+                    resetLockoutRequiresHardwareAuthToken,
+                    List.of(new SensorLocationInternal("" /* display */, udfpsProps[0],
+                            udfpsProps[1], udfpsProps[2])));
         } else {
             return new FingerprintSensorPropertiesInternal(sensorId,
                     Utils.authenticatorStrengthToPropertyStrength(strength), maxEnrollmentsPerUser,
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 998a8e1..a600f08 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -176,6 +176,7 @@
                             prop.commonProps.maxEnrollmentsPerUser,
                             componentInfo,
                             prop.sensorType,
+                            prop.halControlsIllumination,
                             true /* resetLockoutRequiresHardwareAuthToken */,
                             !workaroundLocations.isEmpty() ? workaroundLocations :
                                     Arrays.stream(prop.sensorLocations).map(location ->
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index 485a674..bea0f4f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -400,7 +400,7 @@
                 .getInteger(R.integer.config_fingerprintMaxTemplatesPerUser);
         mSensorProperties = new FingerprintSensorPropertiesInternal(sensorProps.sensorId,
                 sensorProps.sensorStrength, maxTemplatesAllowed, sensorProps.componentInfo,
-                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, false /* halControlsIllumination */,
                 resetLockoutRequiresHardwareAuthToken, sensorProps.getAllLocations());
         mMockHalResultController = controller;
         mUserHasTrust = new SparseBooleanArray();
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index f3d41c40..2cf11c6 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1440,16 +1440,6 @@
             DisplayDevice device =
                     mVirtualDisplayAdapter.releaseVirtualDisplayLocked(appToken);
             if (device != null) {
-                final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
-                if (display != null) {
-                    final int displayId = display.getDisplayIdLocked();
-                    if (mDisplayWindowPolicyControllers.contains(displayId)) {
-                        Pair<IVirtualDevice, DisplayWindowPolicyController> pair =
-                                mDisplayWindowPolicyControllers.removeReturnOld(displayId);
-                        getLocalService(VirtualDeviceManagerInternal.class)
-                                .onVirtualDisplayRemoved(pair.first, displayId);
-                    }
-                }
                 // TODO: multi-display - handle virtual displays the same as other display adapters.
                 mDisplayDeviceRepo.onDisplayDeviceEvent(device,
                         DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
@@ -1609,6 +1599,17 @@
         DisplayManagerGlobal.invalidateLocalDisplayInfoCaches();
         sendDisplayEventLocked(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
         scheduleTraversalLocked(false);
+
+        if (mDisplayWindowPolicyControllers.contains(displayId)) {
+            final IVirtualDevice virtualDevice = mDisplayWindowPolicyControllers.removeReturnOld(
+                    displayId).first;
+            if (virtualDevice != null) {
+                mHandler.post(() -> {
+                    getLocalService(VirtualDeviceManagerInternal.class)
+                            .onVirtualDisplayRemoved(virtualDevice, displayId);
+                });
+            }
+        }
     }
 
     private void handleLogicalDisplaySwappedLocked(@NonNull LogicalDisplay display) {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 8ab0b93..5ae3f33 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -164,6 +164,7 @@
     private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4;
     private static final int MSG_RELOAD_DEVICE_ALIASES = 5;
     private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 6;
+    private static final int MSG_POINTER_DISPLAY_ID_CHANGED = 7;
 
     private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
 
@@ -276,11 +277,24 @@
     @GuardedBy("mAssociationLock")
     private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>();
 
+    // Guards per-display input properties and properties relating to the mouse pointer.
+    // Threads can wait on this lock to be notified the next time the display on which the mouse
+    // pointer is shown has changed.
     private final Object mAdditionalDisplayInputPropertiesLock = new Object();
 
-    // Forces the MouseCursorController to target a specific display id.
+    // Forces the PointerController to target a specific display id.
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
     private int mOverriddenPointerDisplayId = Display.INVALID_DISPLAY;
+
+    // PointerController is the source of truth of the pointer display. This is the value of the
+    // latest pointer display id reported by PointerController.
+    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
+    private int mAcknowledgedPointerDisplayId = Display.INVALID_DISPLAY;
+    // This is the latest display id that IMS has requested PointerController to use. If there are
+    // no devices that can control the pointer, PointerController may end up disregarding this
+    // value.
+    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
+    private int mRequestedPointerDisplayId = Display.INVALID_DISPLAY;
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
     private final SparseArray<AdditionalDisplayInputProperties> mAdditionalDisplayInputProperties =
             new SparseArray<>();
@@ -289,7 +303,6 @@
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
     private PointerIcon mIcon;
 
-
     // Holds all the registered gesture monitors that are implemented as spy windows. The spy
     // windows are mapped by their InputChannel tokens.
     @GuardedBy("mInputMonitors")
@@ -383,6 +396,10 @@
         NativeInputManagerService getNativeService(InputManagerService service) {
             return new NativeInputManagerService.NativeImpl(service, mContext, mLooper.getQueue());
         }
+
+        void registerLocalService(InputManagerInternal localService) {
+            LocalServices.addService(InputManagerInternal.class, localService);
+        }
     }
 
     public InputManagerService(Context context) {
@@ -391,11 +408,14 @@
 
     @VisibleForTesting
     InputManagerService(Injector injector) {
+        // The static association map is accessed by both java and native code, so it must be
+        // initialized before initializing the native service.
+        mStaticAssociations = loadStaticInputPortAssociations();
+
         mContext = injector.getContext();
         mHandler = new InputManagerHandler(injector.getLooper());
         mNative = injector.getNativeService(this);
 
-        mStaticAssociations = loadStaticInputPortAssociations();
         mUseDevInputEventForAudioJack =
                 mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
         Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack="
@@ -406,7 +426,7 @@
         mDoubleTouchGestureEnableFile = TextUtils.isEmpty(doubleTouchGestureEnablePath) ? null :
             new File(doubleTouchGestureEnablePath);
 
-        LocalServices.addService(InputManagerInternal.class, new LocalService());
+        injector.registerLocalService(new LocalService());
     }
 
     public void setWindowManagerCallbacks(WindowManagerCallbacks callbacks) {
@@ -556,6 +576,8 @@
                 vArray[i] = viewports.get(i);
             }
             mNative.setDisplayViewports(vArray);
+            // Always attempt to update the pointer display when viewports change.
+            updatePointerDisplayId();
 
             if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
                 final AdditionalDisplayInputProperties properties =
@@ -1961,10 +1983,43 @@
         return result;
     }
 
-    private void setVirtualMousePointerDisplayId(int displayId) {
+    /**
+     * Update the display on which the mouse pointer is shown.
+     * If there is an overridden display for the mouse pointer, use that. Otherwise, query
+     * WindowManager for the pointer display.
+     *
+     * @return true if the pointer displayId changed, false otherwise.
+     */
+    private boolean updatePointerDisplayId() {
+        synchronized (mAdditionalDisplayInputPropertiesLock) {
+            final int pointerDisplayId = mOverriddenPointerDisplayId != Display.INVALID_DISPLAY
+                    ? mOverriddenPointerDisplayId : mWindowManagerCallbacks.getPointerDisplayId();
+            if (mRequestedPointerDisplayId == pointerDisplayId) {
+                return false;
+            }
+            mRequestedPointerDisplayId = pointerDisplayId;
+            mNative.setPointerDisplayId(pointerDisplayId);
+            return true;
+        }
+    }
+
+    private void handlePointerDisplayIdChanged(PointerDisplayIdChangedArgs args) {
+        synchronized (mAdditionalDisplayInputPropertiesLock) {
+            mAcknowledgedPointerDisplayId = args.mPointerDisplayId;
+            // Notify waiting threads that the display of the mouse pointer has changed.
+            mAdditionalDisplayInputPropertiesLock.notifyAll();
+        }
+        mWindowManagerCallbacks.notifyPointerDisplayIdChanged(
+                args.mPointerDisplayId, args.mXPosition, args.mYPosition);
+    }
+
+    private boolean setVirtualMousePointerDisplayIdBlocking(int displayId) {
+        // Indicates whether this request is for removing the override.
+        final boolean removingOverride = displayId == Display.INVALID_DISPLAY;
+
         synchronized (mAdditionalDisplayInputPropertiesLock) {
             mOverriddenPointerDisplayId = displayId;
-            if (displayId != Display.INVALID_DISPLAY) {
+            if (!removingOverride) {
                 final AdditionalDisplayInputProperties properties =
                         mAdditionalDisplayInputProperties.get(displayId);
                 if (properties != null) {
@@ -1972,9 +2027,30 @@
                     updatePointerIconVisibleLocked(properties.pointerIconVisible);
                 }
             }
+            if (!updatePointerDisplayId() && mAcknowledgedPointerDisplayId == displayId) {
+                // The requested pointer display is already set.
+                return true;
+            }
+            if (removingOverride && mAcknowledgedPointerDisplayId == Display.INVALID_DISPLAY) {
+                // The pointer display override is being removed, but the current pointer display
+                // is already invalid. This can happen when the PointerController is destroyed as a
+                // result of the removal of all input devices that can control the pointer.
+                return true;
+            }
+            try {
+                // The pointer display changed, so wait until the change has propagated.
+                mAdditionalDisplayInputPropertiesLock.wait(5_000 /*mills*/);
+            } catch (InterruptedException ignored) {
+            }
+            // This request succeeds in two cases:
+            // - This request was to remove the override, in which case the new pointer display
+            //   could be anything that WM has set.
+            // - We are setting a new override, in which case the request only succeeds if the
+            //   reported new displayId is the one we requested. This check ensures that if two
+            //   competing overrides are requested in succession, the caller can be notified if one
+            //   of them fails.
+            return  removingOverride || mAcknowledgedPointerDisplayId == displayId;
         }
-        // TODO(b/215597605): trigger MousePositionTracker update
-        mNative.notifyPointerDisplayIdChanged();
     }
 
     private int getVirtualMousePointerDisplayId() {
@@ -3156,18 +3232,6 @@
 
     // Native callback.
     @SuppressWarnings("unused")
-    private int getPointerDisplayId() {
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            // Prefer the override to all other displays.
-            if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
-                return mOverriddenPointerDisplayId;
-            }
-        }
-        return mWindowManagerCallbacks.getPointerDisplayId();
-    }
-
-    // Native callback.
-    @SuppressWarnings("unused")
     private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
         if (!mSystemReady) {
             return null;
@@ -3206,6 +3270,26 @@
         return null;
     }
 
+    private static class PointerDisplayIdChangedArgs {
+        final int mPointerDisplayId;
+        final float mXPosition;
+        final float mYPosition;
+        PointerDisplayIdChangedArgs(int pointerDisplayId, float xPosition, float yPosition) {
+            mPointerDisplayId = pointerDisplayId;
+            mXPosition = xPosition;
+            mYPosition = yPosition;
+        }
+    }
+
+    // Native callback.
+    @SuppressWarnings("unused")
+    @VisibleForTesting
+    void onPointerDisplayIdChanged(int pointerDisplayId, float xPosition, float yPosition) {
+        mHandler.obtainMessage(MSG_POINTER_DISPLAY_ID_CHANGED,
+                new PointerDisplayIdChangedArgs(pointerDisplayId, xPosition,
+                        yPosition)).sendToTarget();
+    }
+
     /**
      * Callback interface implemented by the Window Manager.
      */
@@ -3329,6 +3413,14 @@
          */
         @Nullable
         SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
+
+        /**
+         * Notify WindowManagerService when the display of the mouse pointer changes.
+         * @param displayId The display on which the mouse pointer is shown.
+         * @param x The x coordinate of the mouse pointer.
+         * @param y The y coordinate of the mouse pointer.
+         */
+        void notifyPointerDisplayIdChanged(int displayId, float x, float y);
     }
 
     /**
@@ -3381,6 +3473,9 @@
                     boolean inTabletMode = (boolean) args.arg1;
                     deliverTabletModeChanged(whenNanos, inTabletMode);
                     break;
+                case MSG_POINTER_DISPLAY_ID_CHANGED:
+                    handlePointerDisplayIdChanged((PointerDisplayIdChangedArgs) msg.obj);
+                    break;
             }
         }
     }
@@ -3631,8 +3726,9 @@
         }
 
         @Override
-        public void setVirtualMousePointerDisplayId(int pointerDisplayId) {
-            InputManagerService.this.setVirtualMousePointerDisplayId(pointerDisplayId);
+        public boolean setVirtualMousePointerDisplayId(int pointerDisplayId) {
+            return InputManagerService.this
+                    .setVirtualMousePointerDisplayIdBlocking(pointerDisplayId);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 2169155..81882d2 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -176,6 +176,9 @@
 
     void cancelCurrentTouch();
 
+    /** Set the displayId on which the mouse cursor should be shown. */
+    void setPointerDisplayId(int displayId);
+
     /** The native implementation of InputManagerService methods. */
     class NativeImpl implements NativeInputManagerService {
         /** Pointer to native input manager service object, used by native code. */
@@ -388,5 +391,8 @@
 
         @Override
         public native void cancelCurrentTouch();
+
+        @Override
+        public native void setPointerDisplayId(int displayId);
     }
 }
diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
index 79088d0..f9a8407 100644
--- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
+++ b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
@@ -16,26 +16,28 @@
 
 package com.android.server.logcat;
 
+import android.annotation.StyleRes;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.UserHandle;
-import android.os.logcat.ILogcatManagerService;
 import android.util.Slog;
+import android.view.ContextThemeWrapper;
 import android.view.InflateException;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
 import com.android.internal.R;
+import com.android.server.LocalServices;
 
 /**
  * Dialog responsible for obtaining user consent per-use log access
@@ -43,61 +45,61 @@
 public class LogAccessDialogActivity extends Activity implements
         View.OnClickListener {
     private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
-    private Context mContext;
 
-    private final ILogcatManagerService mLogcatManagerService =
-            ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat"));
+    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
+    private static final int MSG_DISMISS_DIALOG = 0;
+
+    private final LogcatManagerService.LogcatManagerServiceInternal mLogcatManagerInternal =
+            LocalServices.getService(LogcatManagerService.LogcatManagerServiceInternal.class);
 
     private String mPackageName;
-
     private int mUid;
-    private int mGid;
-    private int mPid;
-    private int mFd;
+
     private String mAlertTitle;
     private AlertDialog.Builder mAlertDialog;
     private AlertDialog mAlert;
     private View mAlertView;
 
-    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
-    private static final int MSG_DISMISS_DIALOG = 0;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        try {
-            mContext = this;
-
-            // retrieve Intent extra information
-            Intent intent = getIntent();
-            getIntentInfo(intent);
-
-            // retrieve the title string from passed intent extra
-            mAlertTitle = getTitleString(mContext, mPackageName, mUid);
-
-            // creaet View
-            mAlertView = createView();
-
-            // create AlertDialog
-            mAlertDialog = new AlertDialog.Builder(this);
-            mAlertDialog.setView(mAlertView);
-
-            // show Alert
-            mAlert = mAlertDialog.create();
-            mAlert.show();
-
-            // set Alert Timeout
-            mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT);
-
-        } catch (Exception e) {
-            try {
-                Slog.e(TAG, "onCreate failed, declining the logd access", e);
-                mLogcatManagerService.decline(mUid, mGid, mPid, mFd);
-            } catch (RemoteException ex) {
-                Slog.e(TAG, "Fails to call remote functions", ex);
-            }
+        // retrieve Intent extra information
+        if (!readIntentInfo(getIntent())) {
+            Slog.e(TAG, "Invalid Intent extras, finishing");
+            finish();
+            return;
         }
+
+        // retrieve the title string from passed intent extra
+        try {
+            mAlertTitle = getTitleString(this, mPackageName, mUid);
+        } catch (NameNotFoundException e) {
+            Slog.e(TAG, "Unable to fetch label of package " + mPackageName, e);
+            declineLogAccess();
+            finish();
+            return;
+        }
+
+        // create View
+        boolean isDarkTheme = (getResources().getConfiguration().uiMode
+                & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
+        int themeId = isDarkTheme ? android.R.style.Theme_DeviceDefault_Dialog_Alert :
+                android.R.style.Theme_DeviceDefault_Light_Dialog_Alert;
+        mAlertView = createView(themeId);
+
+        // create AlertDialog
+        mAlertDialog = new AlertDialog.Builder(this, themeId);
+        mAlertDialog.setView(mAlertView);
+        mAlertDialog.setOnCancelListener(dialog -> declineLogAccess());
+        mAlertDialog.setOnDismissListener(dialog -> finish());
+
+        // show Alert
+        mAlert = mAlertDialog.create();
+        mAlert.show();
+
+        // set Alert Timeout
+        mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT);
     }
 
     @Override
@@ -109,21 +111,26 @@
         mAlert = null;
     }
 
-    private void getIntentInfo(Intent intent) throws Exception {
-
+    private boolean readIntentInfo(Intent intent) {
         if (intent == null) {
-            throw new NullPointerException("Intent is null");
+            Slog.e(TAG, "Intent is null");
+            return false;
         }
 
         mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
         if (mPackageName == null || mPackageName.length() == 0) {
-            throw new NullPointerException("Package Name is null");
+            Slog.e(TAG, "Missing package name extra");
+            return false;
         }
 
-        mUid = intent.getIntExtra("com.android.server.logcat.uid", 0);
-        mGid = intent.getIntExtra("com.android.server.logcat.gid", 0);
-        mPid = intent.getIntExtra("com.android.server.logcat.pid", 0);
-        mFd = intent.getIntExtra("com.android.server.logcat.fd", 0);
+        if (!intent.hasExtra(Intent.EXTRA_UID)) {
+            Slog.e(TAG, "Missing EXTRA_UID");
+            return false;
+        }
+
+        mUid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+
+        return true;
     }
 
     private Handler mHandler = new Handler() {
@@ -133,11 +140,7 @@
                     if (mAlert != null) {
                         mAlert.dismiss();
                         mAlert = null;
-                        try {
-                            mLogcatManagerService.decline(mUid, mGid, mPid, mFd);
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "Fails to call remote functions", e);
-                        }
+                        declineLogAccess();
                     }
                     break;
 
@@ -148,25 +151,15 @@
     };
 
     private String getTitleString(Context context, String callingPackage, int uid)
-            throws Exception {
-
+            throws NameNotFoundException {
         PackageManager pm = context.getPackageManager();
-        if (pm == null) {
-            throw new NullPointerException("PackageManager is null");
-        }
 
         CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage,
                 PackageManager.MATCH_DIRECT_BOOT_AUTO,
                 UserHandle.getUserId(uid)).loadLabel(pm);
-        if (appLabel == null || appLabel.length() == 0) {
-            throw new NameNotFoundException("Application Label is null");
-        }
 
         String titleString = context.getString(
                 com.android.internal.R.string.log_access_confirmation_title, appLabel);
-        if (titleString == null || titleString.length() == 0) {
-            throw new NullPointerException("Title is null");
-        }
 
         return titleString;
     }
@@ -176,9 +169,9 @@
      * If we cannot retrieve the package name, it returns null and we decline the full device log
      * access
      */
-    private View createView() throws Exception {
-
-        final View view = getLayoutInflater().inflate(
+    private View createView(@StyleRes int themeId) {
+        Context themedContext = new ContextThemeWrapper(getApplicationContext(), themeId);
+        final View view = LayoutInflater.from(themedContext).inflate(
                 R.layout.log_access_user_consent_dialog_permission, null /*root*/);
 
         if (view == null) {
@@ -202,21 +195,17 @@
     public void onClick(View view) {
         switch (view.getId()) {
             case R.id.log_access_dialog_allow_button:
-                try {
-                    mLogcatManagerService.approve(mUid, mGid, mPid, mFd);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Fails to call remote functions", e);
-                }
+                mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName);
                 finish();
                 break;
             case R.id.log_access_dialog_deny_button:
-                try {
-                    mLogcatManagerService.decline(mUid, mGid, mPid, mFd);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Fails to call remote functions", e);
-                }
+                declineLogAccess();
                 finish();
                 break;
         }
     }
+
+    private void declineLogAccess() {
+        mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName);
+    }
 }
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index 5dccd07..21beb96 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -16,90 +16,322 @@
 
 package com.android.server.logcat;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Handler;
 import android.os.ILogd;
+import android.os.Looper;
+import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.logcat.ILogcatManagerService;
+import android.util.ArrayMap;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
 
 
 /**
  * Service responsible for managing the access to Logcat.
  */
 public final class LogcatManagerService extends SystemService {
-
     private static final String TAG = "LogcatManagerService";
+    private static final boolean DEBUG = false;
+
+    /** How long to wait for the user to approve/decline before declining automatically */
+    @VisibleForTesting
+    static final int PENDING_CONFIRMATION_TIMEOUT_MILLIS = Build.IS_DEBUGGABLE ? 70000 : 400000;
+
+    /**
+     * How long an approved / declined status is valid for.
+     *
+     * After a client has been approved/declined log access, if they try to access logs again within
+     * this timeout, the new request will be automatically approved/declined.
+     * Only after this timeout expires will a new request generate another prompt to the user.
+     **/
+    @VisibleForTesting
+    static final int STATUS_EXPIRATION_TIMEOUT_MILLIS = 60 * 1000;
+
+    private static final int MSG_LOG_ACCESS_REQUESTED = 0;
+    private static final int MSG_APPROVE_LOG_ACCESS = 1;
+    private static final int MSG_DECLINE_LOG_ACCESS = 2;
+    private static final int MSG_LOG_ACCESS_FINISHED = 3;
+    private static final int MSG_PENDING_TIMEOUT = 4;
+    private static final int MSG_LOG_ACCESS_STATUS_EXPIRED = 5;
+
+    private static final int STATUS_NEW_REQUEST = 0;
+    private static final int STATUS_PENDING = 1;
+    private static final int STATUS_APPROVED = 2;
+    private static final int STATUS_DECLINED = 3;
+
+    @IntDef(prefix = {"STATUS_"}, value = {
+            STATUS_NEW_REQUEST,
+            STATUS_PENDING,
+            STATUS_APPROVED,
+            STATUS_DECLINED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LogAccessRequestStatus {
+    }
+
     private final Context mContext;
+    private final Injector mInjector;
+    private final Supplier<Long> mClock;
     private final BinderService mBinderService;
-    private final ExecutorService mThreadExecutor;
-    private ILogd mLogdService;
-    private @NonNull ActivityManager mActivityManager;
+    private final LogcatManagerServiceInternal mLocalService;
+    private final Handler mHandler;
     private ActivityManagerInternal mActivityManagerInternal;
-    private static final int MAX_UID_IMPORTANCE_COUNT_LISTENER = 2;
-    private static final String TARGET_PACKAGE_NAME = "android";
-    private static final String TARGET_ACTIVITY_NAME =
-            "com.android.server.logcat.LogAccessDialogActivity";
-    private static final String EXTRA_UID = "com.android.server.logcat.uid";
-    private static final String EXTRA_GID = "com.android.server.logcat.gid";
-    private static final String EXTRA_PID = "com.android.server.logcat.pid";
-    private static final String EXTRA_FD = "com.android.server.logcat.fd";
+    private ILogd mLogdService;
+
+    private static final class LogAccessClient {
+        final int mUid;
+        @NonNull
+        final String mPackageName;
+
+        LogAccessClient(int uid, @NonNull String packageName) {
+            mUid = uid;
+            mPackageName = packageName;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof LogAccessClient)) return false;
+            LogAccessClient that = (LogAccessClient) o;
+            return mUid == that.mUid && Objects.equals(mPackageName, that.mPackageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUid, mPackageName);
+        }
+
+        @Override
+        public String toString() {
+            return "LogAccessClient{"
+                    + "mUid=" + mUid
+                    + ", mPackageName=" + mPackageName
+                    + '}';
+        }
+    }
+
+    private static final class LogAccessRequest {
+        final int mUid;
+        final int mGid;
+        final int mPid;
+        final int mFd;
+
+        private LogAccessRequest(int uid, int gid, int pid, int fd) {
+            mUid = uid;
+            mGid = gid;
+            mPid = pid;
+            mFd = fd;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof LogAccessRequest)) return false;
+            LogAccessRequest that = (LogAccessRequest) o;
+            return mUid == that.mUid && mGid == that.mGid && mPid == that.mPid && mFd == that.mFd;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUid, mGid, mPid, mFd);
+        }
+
+        @Override
+        public String toString() {
+            return "LogAccessRequest{"
+                    + "mUid=" + mUid
+                    + ", mGid=" + mGid
+                    + ", mPid=" + mPid
+                    + ", mFd=" + mFd
+                    + '}';
+        }
+    }
+
+    private static final class LogAccessStatus {
+        @LogAccessRequestStatus
+        int mStatus = STATUS_NEW_REQUEST;
+        final List<LogAccessRequest> mPendingRequests = new ArrayList<>();
+    }
+
+    private final Map<LogAccessClient, LogAccessStatus> mLogAccessStatus = new ArrayMap<>();
+    private final Map<LogAccessClient, Integer> mActiveLogAccessCount = new ArrayMap<>();
 
     private final class BinderService extends ILogcatManagerService.Stub {
         @Override
         public void startThread(int uid, int gid, int pid, int fd) {
-            mThreadExecutor.execute(new LogdMonitor(uid, gid, pid, fd, true));
+            final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd);
+            if (DEBUG) {
+                Slog.d(TAG, "New log access request: " + logAccessRequest);
+            }
+            final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_REQUESTED, logAccessRequest);
+            mHandler.sendMessageAtTime(msg, mClock.get());
         }
 
         @Override
         public void finishThread(int uid, int gid, int pid, int fd) {
-            // TODO This thread will be used to notify the AppOpsManager that
-            // the logd data access is finished.
-            mThreadExecutor.execute(new LogdMonitor(uid, gid, pid, fd, false));
+            final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd);
+            if (DEBUG) {
+                Slog.d(TAG, "Log access finished: " + logAccessRequest);
+            }
+            final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_FINISHED, logAccessRequest);
+            mHandler.sendMessageAtTime(msg, mClock.get());
+        }
+    }
+
+    final class LogcatManagerServiceInternal {
+        public void approveAccessForClient(int uid, @NonNull String packageName) {
+            final LogAccessClient client = new LogAccessClient(uid, packageName);
+            if (DEBUG) {
+                Slog.d(TAG, "Approving log access for client: " + client);
+            }
+            final Message msg = mHandler.obtainMessage(MSG_APPROVE_LOG_ACCESS, client);
+            mHandler.sendMessageAtTime(msg, mClock.get());
         }
 
-        @Override
-        public void approve(int uid, int gid, int pid, int fd) {
-            try {
-                Slog.d(TAG, "Allow logd access for uid: " + uid);
-                getLogdService().approve(uid, gid, pid, fd);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Fails to call remote functions", e);
+        public void declineAccessForClient(int uid, @NonNull String packageName) {
+            final LogAccessClient client = new LogAccessClient(uid, packageName);
+            if (DEBUG) {
+                Slog.d(TAG, "Declining log access for client: " + client);
             }
-        }
-
-        @Override
-        public void decline(int uid, int gid, int pid, int fd) {
-            try {
-                Slog.d(TAG, "Decline logd access for uid: " + uid);
-                getLogdService().decline(uid, gid, pid, fd);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Fails to call remote functions", e);
-            }
+            final Message msg = mHandler.obtainMessage(MSG_DECLINE_LOG_ACCESS, client);
+            mHandler.sendMessageAtTime(msg, mClock.get());
         }
     }
 
     private ILogd getLogdService() {
-        synchronized (LogcatManagerService.this) {
-            if (mLogdService == null) {
-                LogcatManagerService.this.addLogdService();
-            }
-            return mLogdService;
+        if (mLogdService == null) {
+            mLogdService = mInjector.getLogdService();
         }
+        return mLogdService;
+    }
+
+    private static class LogAccessRequestHandler extends Handler {
+        private final LogcatManagerService mService;
+
+        LogAccessRequestHandler(Looper looper, LogcatManagerService service) {
+            super(looper);
+            mService = service;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_LOG_ACCESS_REQUESTED: {
+                    LogAccessRequest request = (LogAccessRequest) msg.obj;
+                    mService.onLogAccessRequested(request);
+                    break;
+                }
+                case MSG_APPROVE_LOG_ACCESS: {
+                    LogAccessClient client = (LogAccessClient) msg.obj;
+                    mService.onAccessApprovedForClient(client);
+                    break;
+                }
+                case MSG_DECLINE_LOG_ACCESS: {
+                    LogAccessClient client = (LogAccessClient) msg.obj;
+                    mService.onAccessDeclinedForClient(client);
+                    break;
+                }
+                case MSG_LOG_ACCESS_FINISHED: {
+                    LogAccessRequest request = (LogAccessRequest) msg.obj;
+                    mService.onLogAccessFinished(request);
+                    break;
+                }
+                case MSG_PENDING_TIMEOUT: {
+                    LogAccessClient client = (LogAccessClient) msg.obj;
+                    mService.onPendingTimeoutExpired(client);
+                    break;
+                }
+                case MSG_LOG_ACCESS_STATUS_EXPIRED: {
+                    LogAccessClient client = (LogAccessClient) msg.obj;
+                    mService.onAccessStatusExpired(client);
+                    break;
+                }
+            }
+        }
+    }
+
+    static class Injector {
+        protected Supplier<Long> createClock() {
+            return SystemClock::uptimeMillis;
+        }
+
+        protected Looper getLooper() {
+            return Looper.getMainLooper();
+        }
+
+        protected ILogd getLogdService() {
+            return ILogd.Stub.asInterface(ServiceManager.getService("logd"));
+        }
+    }
+
+    public LogcatManagerService(Context context) {
+        this(context, new Injector());
+    }
+
+    public LogcatManagerService(Context context, Injector injector) {
+        super(context);
+        mContext = context;
+        mInjector = injector;
+        mClock = injector.createClock();
+        mBinderService = new BinderService();
+        mLocalService = new LogcatManagerServiceInternal();
+        mHandler = new LogAccessRequestHandler(injector.getLooper(), this);
+    }
+
+    @Override
+    public void onStart() {
+        try {
+            mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+            publishBinderService("logcat", mBinderService);
+            publishLocalService(LogcatManagerServiceInternal.class, mLocalService);
+        } catch (Throwable t) {
+            Slog.e(TAG, "Could not start the LogcatManagerService.", t);
+        }
+    }
+
+    @VisibleForTesting
+    LogcatManagerServiceInternal getLocalService() {
+        return mLocalService;
+    }
+
+    @VisibleForTesting
+    ILogcatManagerService getBinderService() {
+        return mBinderService;
+    }
+
+    @Nullable
+    private LogAccessClient getClientForRequest(LogAccessRequest request) {
+        final String packageName = getPackageName(request);
+        if (packageName == null) {
+            return null;
+        }
+
+        return new LogAccessClient(request.mUid, packageName);
     }
 
     /**
@@ -107,12 +339,9 @@
      * If we cannot retrieve the package name, it returns null and we decline the full device log
      * access
      */
-    private String getPackageName(int uid, int gid, int pid, int fd) {
-
-        final ActivityManagerInternal activityManagerInternal =
-                LocalServices.getService(ActivityManagerInternal.class);
-        if (activityManagerInternal != null) {
-            String packageName = activityManagerInternal.getPackageNameByPid(pid);
+    private String getPackageName(LogAccessRequest request) {
+        if (mActivityManagerInternal != null) {
+            String packageName = mActivityManagerInternal.getPackageNameByPid(request.mPid);
             if (packageName != null) {
                 return packageName;
             }
@@ -125,7 +354,7 @@
             return null;
         }
 
-        String[] packageNames = pm.getPackagesForUid(uid);
+        String[] packageNames = pm.getPackagesForUid(request.mUid);
 
         if (ArrayUtils.isEmpty(packageNames)) {
             // Decline the logd access if the app name is unknown
@@ -142,127 +371,164 @@
         }
 
         return firstPackageName;
-
     }
 
-    private void declineLogdAccess(int uid, int gid, int pid, int fd) {
+    void onLogAccessRequested(LogAccessRequest request) {
+        final LogAccessClient client = getClientForRequest(request);
+        if (client == null) {
+            declineRequest(request);
+            return;
+        }
+
+        LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
+        if (logAccessStatus == null) {
+            logAccessStatus = new LogAccessStatus();
+            mLogAccessStatus.put(client, logAccessStatus);
+        }
+
+        switch (logAccessStatus.mStatus) {
+            case STATUS_NEW_REQUEST:
+                logAccessStatus.mPendingRequests.add(request);
+                processNewLogAccessRequest(client);
+                break;
+            case STATUS_PENDING:
+                logAccessStatus.mPendingRequests.add(request);
+                return;
+            case STATUS_APPROVED:
+                approveRequest(client, request);
+                break;
+            case STATUS_DECLINED:
+                declineRequest(request);
+                break;
+        }
+    }
+
+    private boolean shouldShowConfirmationDialog(LogAccessClient client) {
+        // If the process is foreground, show a dialog for user consent
+        final int procState = mActivityManagerInternal.getUidProcessState(client.mUid);
+        return procState == ActivityManager.PROCESS_STATE_TOP;
+    }
+
+    private void processNewLogAccessRequest(LogAccessClient client) {
+        boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid);
+
+        // The instrumented apks only run for testing, so we don't check user permission.
+        if (isInstrumented) {
+            onAccessApprovedForClient(client);
+            return;
+        }
+
+        if (!shouldShowConfirmationDialog(client)) {
+            onAccessDeclinedForClient(client);
+            return;
+        }
+
+        final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
+        logAccessStatus.mStatus = STATUS_PENDING;
+
+        mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client),
+                mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS);
+        final Intent mIntent = createIntent(client);
+        mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM);
+    }
+
+    void onAccessApprovedForClient(LogAccessClient client) {
+        scheduleStatusExpiry(client);
+
+        LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
+        if (logAccessStatus != null) {
+            for (LogAccessRequest request : logAccessStatus.mPendingRequests) {
+                approveRequest(client, request);
+            }
+            logAccessStatus.mStatus = STATUS_APPROVED;
+            logAccessStatus.mPendingRequests.clear();
+        }
+    }
+
+    void onAccessDeclinedForClient(LogAccessClient client) {
+        scheduleStatusExpiry(client);
+
+        LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
+        if (logAccessStatus != null) {
+            for (LogAccessRequest request : logAccessStatus.mPendingRequests) {
+                declineRequest(request);
+            }
+            logAccessStatus.mStatus = STATUS_DECLINED;
+            logAccessStatus.mPendingRequests.clear();
+        }
+    }
+
+    private void scheduleStatusExpiry(LogAccessClient client) {
+        mHandler.removeMessages(MSG_PENDING_TIMEOUT, client);
+        mHandler.removeMessages(MSG_LOG_ACCESS_STATUS_EXPIRED, client);
+        mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_LOG_ACCESS_STATUS_EXPIRED, client),
+                mClock.get() + STATUS_EXPIRATION_TIMEOUT_MILLIS);
+    }
+
+    void onPendingTimeoutExpired(LogAccessClient client) {
+        final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
+        if (logAccessStatus != null && logAccessStatus.mStatus == STATUS_PENDING) {
+            onAccessDeclinedForClient(client);
+        }
+    }
+
+    void onAccessStatusExpired(LogAccessClient client) {
+        if (DEBUG) {
+            Slog.d(TAG, "Log access status expired for " + client);
+        }
+        mLogAccessStatus.remove(client);
+    }
+
+    void onLogAccessFinished(LogAccessRequest request) {
+        final LogAccessClient client = getClientForRequest(request);
+        final int activeCount = mActiveLogAccessCount.getOrDefault(client, 1) - 1;
+
+        if (activeCount == 0) {
+            mActiveLogAccessCount.remove(client);
+            if (DEBUG) {
+                Slog.d(TAG, "Client is no longer accessing logs: " + client);
+            }
+            // TODO This will be used to notify the AppOpsManager that the logd data access
+            // is finished.
+        } else {
+            mActiveLogAccessCount.put(client, activeCount);
+        }
+    }
+
+    private void approveRequest(LogAccessClient client, LogAccessRequest request) {
+        if (DEBUG) {
+            Slog.d(TAG, "Approving log access: " + request);
+        }
         try {
-            getLogdService().decline(uid, gid, pid, fd);
+            getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd);
+            Integer activeCount = mActiveLogAccessCount.getOrDefault(client, 0);
+            mActiveLogAccessCount.put(client, activeCount + 1);
         } catch (RemoteException e) {
             Slog.e(TAG, "Fails to call remote functions", e);
         }
     }
 
-    private static String getClientInfo(int uid, int gid, int pid, int fd) {
-        return "UID=" + Integer.toString(uid) + " GID=" + Integer.toString(gid) + " PID="
-                + Integer.toString(pid) + " FD=" + Integer.toString(fd);
-    }
-
-    private class LogdMonitor implements Runnable {
-
-        private final int mUid;
-        private final int mGid;
-        private final int mPid;
-        private final int mFd;
-        private final boolean mStart;
-
-        /**
-         * For starting a thread, the start value is true.
-         * For finishing a thread, the start value is false.
-         */
-        LogdMonitor(int uid, int gid, int pid, int fd, boolean start) {
-            mUid = uid;
-            mGid = gid;
-            mPid = pid;
-            mFd = fd;
-            mStart = start;
+    private void declineRequest(LogAccessRequest request) {
+        if (DEBUG) {
+            Slog.d(TAG, "Declining log access: " + request);
         }
-
-        /**
-         * LogdMonitor generates a prompt for users.
-         * The users decide whether the logd access is allowed.
-         */
-        @Override
-        public void run() {
-            if (mLogdService == null) {
-                LogcatManagerService.this.addLogdService();
-            }
-
-            if (mStart) {
-
-                ActivityManagerInternal ami = LocalServices.getService(
-                        ActivityManagerInternal.class);
-                boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(mUid);
-
-                // The instrumented apks only run for testing, so we don't check user permission.
-                if (isCallerInstrumented) {
-                    try {
-                        getLogdService().approve(mUid, mGid, mPid, mFd);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "Fails to call remote functions", e);
-                    }
-                    return;
-                }
-
-                final int procState = LocalServices.getService(ActivityManagerInternal.class)
-                        .getUidProcessState(mUid);
-                // If the process is foreground and we can retrieve the package name, show a dialog
-                // for user consent
-                if (procState == ActivityManager.PROCESS_STATE_TOP) {
-                    String packageName = getPackageName(mUid, mGid, mPid, mFd);
-                    if (packageName != null) {
-                        final Intent mIntent = createIntent(packageName, mUid, mGid, mPid, mFd);
-                        mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM);
-                        return;
-                    }
-                }
-
-                /**
-                 * If the process is background or cannot retrieve the package name,
-                 * decline the logd access.
-                 **/
-                declineLogdAccess(mUid, mGid, mPid, mFd);
-                return;
-            }
-        }
-    }
-
-    public LogcatManagerService(Context context) {
-        super(context);
-        mContext = context;
-        mBinderService = new BinderService();
-        mThreadExecutor = Executors.newCachedThreadPool();
-        mActivityManager = context.getSystemService(ActivityManager.class);
-    }
-
-    @Override
-    public void onStart() {
         try {
-            publishBinderService("logcat", mBinderService);
-        } catch (Throwable t) {
-            Slog.e(TAG, "Could not start the LogcatManagerService.", t);
+            getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Fails to call remote functions", e);
         }
     }
 
-    private void addLogdService() {
-        mLogdService = ILogd.Stub.asInterface(ServiceManager.getService("logd"));
-    }
-
     /**
      * Create the Intent for LogAccessDialogActivity.
      */
-    public Intent createIntent(String targetPackageName, int uid, int gid, int pid, int fd) {
-        final Intent intent = new Intent();
+    public Intent createIntent(LogAccessClient client) {
+        final Intent intent = new Intent(mContext, LogAccessDialogActivity.class);
 
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 
-        intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPackageName);
-        intent.putExtra(EXTRA_UID, uid);
-        intent.putExtra(EXTRA_GID, gid);
-        intent.putExtra(EXTRA_PID, pid);
-        intent.putExtra(EXTRA_FD, fd);
-
-        intent.setComponent(new ComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME));
+        intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName);
+        intent.putExtra(Intent.EXTRA_UID, client.mUid);
 
         return intent;
     }
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 02f9ceb..89902f7 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -51,14 +51,16 @@
     void onNotificationSettingsViewed(String key);
     /**
      * Called when the state of {@link Notification#FLAG_BUBBLE} is changed.
+     *
+     * @param key the notification key
+     * @param isBubble whether the notification should have {@link Notification#FLAG_BUBBLE} applied
+     * @param flags the flags to apply to the notification's {@link Notification.BubbleMetadata}
      */
     void onNotificationBubbleChanged(String key, boolean isBubble, int flags);
     /**
-     * Called when the state of {@link Notification.BubbleMetadata#FLAG_SUPPRESS_NOTIFICATION}
-     * or {@link Notification.BubbleMetadata#FLAG_SUPPRESS_BUBBLE} changes.
+     * Called when the flags on {@link Notification.BubbleMetadata} are changed.
      */
-    void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed,
-            boolean isBubbleSuppressed);
+    void onBubbleMetadataFlagChanged(String key, int flags);
 
     /**
      * Grant permission to read the specified URI to the package associated with the
diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
index c548e7e..8a62736 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
@@ -42,4 +42,7 @@
 
     /** Does the specified package/uid have permission to post notifications? */
     boolean areNotificationsEnabledForPackage(String pkg, int uid);
+
+    /** Send a notification to the user prompting them to review their notification permissions. */
+    void sendReviewPermissionsNotification();
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6078bfc..83c576e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
 import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
@@ -274,6 +275,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.SomeArgs;
@@ -442,6 +444,18 @@
 
     private static final int NOTIFICATION_INSTANCE_ID_MAX = (1 << 13);
 
+    // States for the review permissions notification
+    static final int REVIEW_NOTIF_STATE_UNKNOWN = -1;
+    static final int REVIEW_NOTIF_STATE_SHOULD_SHOW = 0;
+    static final int REVIEW_NOTIF_STATE_USER_INTERACTED = 1;
+    static final int REVIEW_NOTIF_STATE_DISMISSED = 2;
+    static final int REVIEW_NOTIF_STATE_RESHOWN = 3;
+
+    // Action strings for review permissions notification
+    static final String REVIEW_NOTIF_ACTION_REMIND = "REVIEW_NOTIF_ACTION_REMIND";
+    static final String REVIEW_NOTIF_ACTION_DISMISS = "REVIEW_NOTIF_ACTION_DISMISS";
+    static final String REVIEW_NOTIF_ACTION_CANCELED = "REVIEW_NOTIF_ACTION_CANCELED";
+
     /**
      * Apps that post custom toasts in the background will have those blocked. Apps can
      * still post toasts created with
@@ -652,6 +666,9 @@
     private InstanceIdSequence mNotificationInstanceIdSequence;
     private Set<String> mMsgPkgsAllowedAsConvos = new HashSet();
 
+    // Broadcast intent receiver for notification permissions review-related intents
+    private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver;
+
     static class Archive {
         final SparseArray<Boolean> mEnabled;
         final int mBufferSize;
@@ -1413,8 +1430,7 @@
         }
 
         @Override
-        public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed,
-                boolean isBubbleSuppressed) {
+        public void onBubbleMetadataFlagChanged(String key, int flags) {
             synchronized (mNotificationLock) {
                 NotificationRecord r = mNotificationsByKey.get(key);
                 if (r != null) {
@@ -1424,17 +1440,12 @@
                         return;
                     }
 
-                    boolean flagChanged = false;
-                    if (data.isNotificationSuppressed() != isNotifSuppressed) {
-                        flagChanged = true;
-                        data.setSuppressNotification(isNotifSuppressed);
-                    }
-                    if (data.isBubbleSuppressed() != isBubbleSuppressed) {
-                        flagChanged = true;
-                        data.setSuppressBubble(isBubbleSuppressed);
-                    }
-                    if (flagChanged) {
+                    if (flags != data.getFlags()) {
+                        data.setFlags(flags);
+                        // Shouldn't alert again just because of a flag change.
                         r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
+                        // Force isAppForeground true here, because for sysui's purposes we
+                        // want to be able to adjust the flag behaviour.
                         mHandler.post(
                                 new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r,
                                         true /* isAppForeground */, SystemClock.elapsedRealtime()));
@@ -2416,6 +2427,11 @@
 
         IntentFilter localeChangedFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
         getContext().registerReceiver(mLocaleChangeReceiver, localeChangedFilter);
+
+        mReviewNotificationPermissionsReceiver = new ReviewNotificationPermissionsReceiver();
+        getContext().registerReceiver(mReviewNotificationPermissionsReceiver,
+                ReviewNotificationPermissionsReceiver.getFilter(),
+                Context.RECEIVER_NOT_EXPORTED);
     }
 
     /**
@@ -2709,6 +2725,7 @@
             mHistoryManager.onBootPhaseAppsCanStart();
             registerDeviceConfigChange();
             migrateDefaultNAS();
+            maybeShowInitialReviewPermissionsNotification();
         } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
             mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis());
         }
@@ -6336,6 +6353,21 @@
         public boolean areNotificationsEnabledForPackage(String pkg, int uid) {
             return areNotificationsEnabledForPackageInt(pkg, uid);
         }
+
+        @Override
+        public void sendReviewPermissionsNotification() {
+            // This method is meant to be called from the JobService upon running the job for this
+            // notification having been rescheduled; so without checking any other state, it will
+            // send the notification.
+            checkCallerIsSystem();
+            NotificationManager nm = getContext().getSystemService(NotificationManager.class);
+            nm.notify(TAG,
+                    SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS,
+                    createReviewPermissionsNotification());
+            Settings.Global.putInt(getContext().getContentResolver(),
+                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                    NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+        }
     };
 
     int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) {
@@ -7145,10 +7177,12 @@
                             && r.getNotification().isBubbleNotification())
                             || (mReason == REASON_CLICK && r.canBubble()
                             && r.isFlagBubbleRemoved())) {
-                        boolean isBubbleSuppressed = r.getNotification().getBubbleMetadata() != null
-                                && r.getNotification().getBubbleMetadata().isBubbleSuppressed();
-                        mNotificationDelegate.onBubbleNotificationSuppressionChanged(
-                                r.getKey(), true /* notifSuppressed */, isBubbleSuppressed);
+                        int flags = 0;
+                        if (r.getNotification().getBubbleMetadata() != null) {
+                            flags = r.getNotification().getBubbleMetadata().getFlags();
+                        }
+                        flags |= FLAG_SUPPRESS_NOTIFICATION;
+                        mNotificationDelegate.onBubbleMetadataFlagChanged(r.getKey(), flags);
                         return;
                     }
                     if ((r.getNotification().flags & mMustHaveFlags) != mMustHaveFlags) {
@@ -11608,6 +11642,76 @@
         out.endTag(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG);
     }
 
+    // Creates a notification that informs the user about changes due to the migration to
+    // use permissions for notifications.
+    protected Notification createReviewPermissionsNotification() {
+        int title = R.string.review_notification_settings_title;
+        int content = R.string.review_notification_settings_text;
+
+        // Tapping on the notification leads to the settings screen for managing app notifications,
+        // using the intent reserved for system services to indicate it comes from this notification
+        Intent tapIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW);
+        Intent remindIntent = new Intent(REVIEW_NOTIF_ACTION_REMIND);
+        Intent dismissIntent = new Intent(REVIEW_NOTIF_ACTION_DISMISS);
+        Intent swipeIntent = new Intent(REVIEW_NOTIF_ACTION_CANCELED);
+
+        // Both "remind me" and "dismiss" actions will be actions received by the BroadcastReceiver
+        final Notification.Action remindMe = new Notification.Action.Builder(null,
+                getContext().getResources().getString(
+                        R.string.review_notification_settings_remind_me_action),
+                PendingIntent.getBroadcast(
+                        getContext(), 0, remindIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+                .build();
+        final Notification.Action dismiss = new Notification.Action.Builder(null,
+                getContext().getResources().getString(
+                        R.string.review_notification_settings_dismiss),
+                PendingIntent.getBroadcast(
+                        getContext(), 0, dismissIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+                .build();
+
+        return new Notification.Builder(getContext(), SystemNotificationChannels.SYSTEM_CHANGES)
+                .setSmallIcon(R.drawable.stat_sys_adb)
+                .setContentTitle(getContext().getResources().getString(title))
+                .setContentText(getContext().getResources().getString(content))
+                .setContentIntent(PendingIntent.getActivity(getContext(), 0, tapIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+                .setStyle(new Notification.BigTextStyle())
+                .setFlag(Notification.FLAG_NO_CLEAR, true)
+                .setAutoCancel(true)
+                .addAction(remindMe)
+                .addAction(dismiss)
+                .setDeleteIntent(PendingIntent.getBroadcast(getContext(), 0, swipeIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+                .build();
+    }
+
+    protected void maybeShowInitialReviewPermissionsNotification() {
+        int currentState = Settings.Global.getInt(getContext().getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                REVIEW_NOTIF_STATE_UNKNOWN);
+
+        // now check the last known state of the notification -- this determination of whether the
+        // user is in the correct target audience occurs elsewhere, and will have written the
+        // REVIEW_NOTIF_STATE_SHOULD_SHOW to indicate it should be shown in the future.
+        //
+        // alternatively, if the user has rescheduled the notification (so it has been shown
+        // again) but not yet interacted with the new notification, then show it again on boot,
+        // as this state indicates that the user had the notification open before rebooting.
+        //
+        // sending the notification here does not record a new state for the notification;
+        // that will be written by parts of the system further down the line if at any point
+        // the user interacts with the notification.
+        if (currentState == REVIEW_NOTIF_STATE_SHOULD_SHOW
+                || currentState == REVIEW_NOTIF_STATE_RESHOWN) {
+            NotificationManager nm = getContext().getSystemService(NotificationManager.class);
+            nm.notify(TAG,
+                    SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS,
+                    createReviewPermissionsNotification());
+        }
+    }
+
     /**
      * Shows a warning on logcat. Shows the toast only once per package. This is to avoid being too
      * aggressive and annoying the user.
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
index a09aa7c..b4230c1 100644
--- a/services/core/java/com/android/server/notification/PermissionHelper.java
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -55,10 +55,8 @@
     private final PermissionManagerServiceInternal mPmi;
     private final IPackageManager mPackageManager;
     private final IPermissionManager mPermManager;
-    // TODO (b/194833441): Remove this boolean (but keep the isMigrationEnabled() method)
-    //  when the migration is enabled
+    // TODO (b/194833441): Remove when the migration is enabled
     private final boolean mMigrationEnabled;
-    private final boolean mIsTv;
     private final boolean mForceUserSetOnUpgrade;
 
     public PermissionHelper(PermissionManagerServiceInternal pmi, IPackageManager packageManager,
@@ -69,17 +67,10 @@
         mPermManager = permManager;
         mMigrationEnabled = migrationEnabled;
         mForceUserSetOnUpgrade = forceUserSetOnUpgrade;
-        boolean isTv;
-        try {
-            isTv = mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK, 0);
-        } catch (RemoteException e) {
-            isTv = false;
-        }
-        mIsTv = isTv;
     }
 
     public boolean isMigrationEnabled() {
-        return mMigrationEnabled && !mIsTv;
+        return mMigrationEnabled;
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 0525b1e..ef3c770 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -96,6 +96,10 @@
     private final int XML_VERSION;
     /** What version to check to do the upgrade for bubbles. */
     private static final int XML_VERSION_BUBBLES_UPGRADE = 1;
+    /** The first xml version with notification permissions enabled. */
+    private static final int XML_VERSION_NOTIF_PERMISSION = 3;
+    /** The first xml version that notifies users to review their notification permissions */
+    private static final int XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION = 4;
     @VisibleForTesting
     static final int UNKNOWN_UID = UserHandle.USER_NULL;
     private static final String NON_BLOCKABLE_CHANNEL_DELIM = ":";
@@ -206,7 +210,7 @@
         mStatsEventBuilderFactory = statsEventBuilderFactory;
 
         if (mPermissionHelper.isMigrationEnabled()) {
-            XML_VERSION = 3;
+            XML_VERSION = 4;
         } else {
             XML_VERSION = 2;
         }
@@ -226,8 +230,16 @@
 
         final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1);
         boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE;
-        boolean migrateToPermission =
-                (xmlVersion < XML_VERSION) && mPermissionHelper.isMigrationEnabled();
+        boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION)
+                && mPermissionHelper.isMigrationEnabled();
+        if (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION) {
+            // make a note that we should show the notification at some point.
+            // it shouldn't be possible for the user to already have seen it, as the XML version
+            // would be newer then.
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                    NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+        }
         ArrayList<PermissionHelper.PackagePermission> pkgPerms = new ArrayList<>();
         synchronized (mPackagePreferences) {
             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java
new file mode 100644
index 0000000..fde45f71
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java
@@ -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.server.notification;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+
+/**
+ * JobService implementation for scheduling the notification informing users about
+ * notification permissions updates and taking them to review their existing permissions.
+ * @hide
+ */
+public class ReviewNotificationPermissionsJobService extends JobService {
+    public static final String TAG = "ReviewNotificationPermissionsJobService";
+
+    @VisibleForTesting
+    protected static final int JOB_ID = 225373531;
+
+    /**
+     *  Schedule a new job that will show a notification the specified amount of time in the future.
+     */
+    public static void scheduleJob(Context context, long rescheduleTimeMillis) {
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        // if the job already exists for some reason, cancel & reschedule
+        if (jobScheduler.getPendingJob(JOB_ID) != null) {
+            jobScheduler.cancel(JOB_ID);
+        }
+        ComponentName component = new ComponentName(
+                context, ReviewNotificationPermissionsJobService.class);
+        JobInfo newJob = new JobInfo.Builder(JOB_ID, component)
+                .setPersisted(true) // make sure it'll still get rescheduled after reboot
+                .setMinimumLatency(rescheduleTimeMillis)  // run after specified amount of time
+                .build();
+        jobScheduler.schedule(newJob);
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        // While jobs typically should be run on different threads, this
+        // job only posts a notification, which is not a long-running operation
+        // as notification posting is asynchronous.
+        NotificationManagerInternal nmi =
+                LocalServices.getService(NotificationManagerInternal.class);
+        nmi.sendReviewPermissionsNotification();
+
+        // once the notification is posted, the job is done, so no need to
+        // keep it alive afterwards
+        return false;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        // If we're interrupted for some reason, try again (though this may not
+        // ever happen due to onStartJob not leaving a job running after being
+        // called)
+        return true;
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java
new file mode 100644
index 0000000..b99aeac
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+
+/**
+ * Broadcast receiver for intents that come from the "review notification permissions" notification,
+ * shown to users who upgrade to T from an earlier OS to inform them of notification setup changes
+ * and invite them to review their notification permissions.
+ */
+public class ReviewNotificationPermissionsReceiver extends BroadcastReceiver {
+    public static final String TAG = "ReviewNotifPermissions";
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // 7 days in millis, as the amount of time to wait before re-sending the notification
+    private static final long JOB_RESCHEDULE_TIME = 1000 /* millis */ * 60 /* seconds */
+            * 60 /* minutes */ * 24 /* hours */ * 7 /* days */;
+
+    static IntentFilter getFilter() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
+        filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS);
+        filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+        return filter;
+    }
+
+    // Cancels the "review notification permissions" notification.
+    @VisibleForTesting
+    protected void cancelNotification(Context context) {
+        NotificationManager nm = context.getSystemService(NotificationManager.class);
+        if (nm != null) {
+            nm.cancel(NotificationManagerService.TAG,
+                    SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS);
+        } else {
+            Slog.w(TAG, "could not cancel notification: NotificationManager not found");
+        }
+    }
+
+    @VisibleForTesting
+    protected void rescheduleNotification(Context context) {
+        ReviewNotificationPermissionsJobService.scheduleJob(context, JOB_RESCHEDULE_TIME);
+        // log if needed
+        if (DEBUG) {
+            Slog.d(TAG, "Scheduled review permissions notification for on or after: "
+                    + LocalDateTime.now(ZoneId.systemDefault())
+                            .plus(JOB_RESCHEDULE_TIME, ChronoUnit.MILLIS));
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND)) {
+            // Reschedule the notification for 7 days in the future
+            rescheduleNotification(context);
+
+            // note that the user has interacted; no longer needed to show the initial
+            // notification
+            Settings.Global.putInt(context.getContentResolver(),
+                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                    NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+            cancelNotification(context);
+        } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS)) {
+            // user dismissed; write to settings so we don't show ever again
+            Settings.Global.putInt(context.getContentResolver(),
+                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                    NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
+            cancelNotification(context);
+        } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED)) {
+            // we may get here from the user swiping away the notification,
+            // or from the notification being canceled in any other way.
+            // only in the case that the user hasn't interacted with it in
+            // any other way yet, reschedule
+            int notifState = Settings.Global.getInt(context.getContentResolver(),
+                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                    /* default */ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
+            if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW) {
+                // user hasn't interacted in the past, so reschedule once and then note that the
+                // user *has* interacted now so we don't re-reschedule if they swipe again
+                rescheduleNotification(context);
+                Settings.Global.putInt(context.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+            } else if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN) {
+                // swiping away on a rescheduled notification; mark as interacted and
+                // don't reschedule again.
+                Settings.Global.putInt(context.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/AppIdSettingMap.java b/services/core/java/com/android/server/pm/AppIdSettingMap.java
index b41a0b8..20c67f8 100644
--- a/services/core/java/com/android/server/pm/AppIdSettingMap.java
+++ b/services/core/java/com/android/server/pm/AppIdSettingMap.java
@@ -19,6 +19,7 @@
 import android.os.Process;
 import android.util.Log;
 
+import com.android.server.utils.SnapshotCache;
 import com.android.server.utils.WatchedArrayList;
 import com.android.server.utils.WatchedSparseArray;
 import com.android.server.utils.Watcher;
@@ -34,10 +35,28 @@
      * index to the corresponding SettingBase object is (appId - FIRST_APPLICATION_ID). If an app ID
      * doesn't exist (i.e., app is not installed), we fill the corresponding entry with null.
      */
-    private WatchedArrayList<SettingBase> mNonSystemSettings = new WatchedArrayList<>();
-    private WatchedSparseArray<SettingBase> mSystemSettings = new WatchedSparseArray<>();
+    private final WatchedArrayList<SettingBase> mNonSystemSettings;
+    private final SnapshotCache<WatchedArrayList<SettingBase>> mNonSystemSettingsSnapshot;
+    private final WatchedSparseArray<SettingBase> mSystemSettings;
+    private final SnapshotCache<WatchedSparseArray<SettingBase>> mSystemSettingsSnapshot;
     private int mFirstAvailableAppId = Process.FIRST_APPLICATION_UID;
 
+    AppIdSettingMap() {
+        mNonSystemSettings = new WatchedArrayList<>();
+        mNonSystemSettingsSnapshot = new SnapshotCache.Auto<>(
+                mNonSystemSettings, mNonSystemSettings, "AppIdSettingMap.mNonSystemSettings");
+        mSystemSettings = new WatchedSparseArray<>();
+        mSystemSettingsSnapshot = new SnapshotCache.Auto<>(
+                mSystemSettings, mSystemSettings, "AppIdSettingMap.mSystemSettings");
+    }
+
+    AppIdSettingMap(AppIdSettingMap orig) {
+        mNonSystemSettings = orig.mNonSystemSettingsSnapshot.snapshot();
+        mNonSystemSettingsSnapshot = new SnapshotCache.Sealed<>();
+        mSystemSettings = orig.mSystemSettingsSnapshot.snapshot();
+        mSystemSettingsSnapshot = new SnapshotCache.Sealed<>();
+    }
+
     /** Returns true if the requested AppID was valid and not already registered. */
     public boolean registerExistingAppId(int appId, SettingBase setting, Object name) {
         if (appId >= Process.FIRST_APPLICATION_UID) {
@@ -134,10 +153,7 @@
     }
 
     public AppIdSettingMap snapshot() {
-        AppIdSettingMap l = new AppIdSettingMap();
-        mNonSystemSettings.snapshot(l.mNonSystemSettings, mNonSystemSettings);
-        mSystemSettings.snapshot(l.mSystemSettings, mSystemSettings);
-        return l;
+        return new AppIdSettingMap(this);
     }
 
     public void registerObserver(Watcher observer) {
diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
index 89f8be2..daac7c0 100644
--- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
+++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -236,13 +236,6 @@
 
         verifyActivityCanHandleIntent(launchIntent, callingUid, userId);
 
-        // Always show the cross profile animation
-        if (options == null) {
-            options = ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle();
-        } else {
-            options.putAll(ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle());
-        }
-
         mInjector.getActivityTaskManagerInternal()
                 .startActivityAsUser(
                         caller,
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 8c33dd9..f1296e0 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -29,7 +29,9 @@
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getDefaultCompilerFilter;
 import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_NULL_PKG;
 
+import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.content.Intent;
@@ -42,6 +44,7 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
@@ -250,10 +253,60 @@
                 numberOfPackagesFailed};
     }
 
+    /**
+     * Checks if system UI package (typically "com.android.systemui") needs to be re-compiled, and
+     * compiles it if needed.
+     */
+    private void checkAndDexOptSystemUi() {
+        Computer snapshot = mPm.snapshotComputer();
+        String sysUiPackageName =
+                mPm.mContext.getString(com.android.internal.R.string.config_systemUi);
+        AndroidPackage pkg = snapshot.getPackage(sysUiPackageName);
+        if (pkg == null) {
+            Log.w(TAG, "System UI package " + sysUiPackageName + " is not found for dexopting");
+            return;
+        }
+
+        boolean useProfileForDexopt = false;
+        File profileFile = new File(getPrebuildProfilePath(pkg));
+        // Copy the profile to the reference profile path if it exists. Installd can only use a
+        // profile at the reference profile path for dexopt.
+        if (profileFile.exists()) {
+            try {
+                synchronized (mPm.mInstallLock) {
+                    if (mPm.mInstaller.copySystemProfile(profileFile.getAbsolutePath(),
+                                pkg.getUid(), pkg.getPackageName(),
+                                ArtManager.getProfileName(null))) {
+                        useProfileForDexopt = true;
+                    } else {
+                        Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath());
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath(), e);
+            }
+        }
+
+        // It could also be after mainline update, but we're not introducing a new reason just for
+        // this special case.
+        performDexOptTraced(new DexoptOptions(pkg.getPackageName(), REASON_BOOT_AFTER_OTA,
+                useProfileForDexopt ? "speed-profile" : "speed", null /* splitName */,
+                0 /* dexoptFlags */));
+    }
+
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public void performPackageDexOptUpgradeIfNeeded() {
         PackageManagerServiceUtils.enforceSystemOrRoot(
                 "Only the system can request package update");
 
+        // The default is "true".
+        if (!"false".equals(DeviceConfig.getProperty("runtime", "dexopt_system_ui_on_boot"))) {
+            // System UI is important to user experience, so we check it on every boot. It may need
+            // to be re-compiled after a mainline update or an OTA.
+            // TODO(b/227310505): Only do this after a mainline update or an OTA.
+            checkAndDexOptSystemUi();
+        }
+
         // We need to re-extract after an OTA.
         boolean causeUpgrade = mPm.isDeviceUpgrading();
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index fef6ce1..4df54b7 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -349,7 +349,7 @@
     private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) {
         final ShortcutInfo shortcut = mShortcuts.remove(id);
         if (shortcut != null) {
-            mShortcutUser.mService.removeIconLocked(shortcut);
+            removeIcon(shortcut);
             shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
                     | ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED_ALL);
         }
@@ -366,7 +366,7 @@
         forceDeleteShortcutInner(newShortcut.getId());
 
         // Extract Icon and update the icon res ID and the bitmap path.
-        s.saveIconAndFixUpShortcutLocked(newShortcut);
+        s.saveIconAndFixUpShortcutLocked(this, newShortcut);
         s.fixUpShortcutResourceNamesAndValues(newShortcut);
         saveShortcut(newShortcut);
     }
@@ -972,7 +972,8 @@
     /**
      * Return the filenames (excluding path names) of icon bitmap files from this package.
      */
-    public ArraySet<String> getUsedBitmapFiles() {
+    @GuardedBy("mLock")
+    private ArraySet<String> getUsedBitmapFilesLocked() {
         final ArraySet<String> usedFiles = new ArraySet<>(1);
         forEachShortcut(si -> {
             if (si.getBitmapPath() != null) {
@@ -982,6 +983,26 @@
         return usedFiles;
     }
 
+    public void cleanupDanglingBitmapFiles(@NonNull File path) {
+        synchronized (mLock) {
+            mShortcutBitmapSaver.waitForAllSavesLocked();
+            final ArraySet<String> usedFiles = getUsedBitmapFilesLocked();
+
+            for (File child : path.listFiles()) {
+                if (!child.isFile()) {
+                    continue;
+                }
+                final String name = child.getName();
+                if (!usedFiles.contains(name)) {
+                    if (ShortcutService.DEBUG) {
+                        Slog.d(TAG, "Removing dangling bitmap file: " + child.getAbsolutePath());
+                    }
+                    child.delete();
+                }
+            }
+        }
+    }
+
     private static String getFileName(@NonNull String path) {
         final int sep = path.lastIndexOf(File.separatorChar);
         if (sep == -1) {
@@ -1608,6 +1629,11 @@
         pw.print(" (");
         pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize[0]));
         pw.println(")");
+
+        pw.println();
+        synchronized (mLock) {
+            mShortcutBitmapSaver.dumpLocked(pw, "  ");
+        }
     }
 
     public void dumpShortcuts(@NonNull PrintWriter pw, int matchFlags) {
@@ -1729,7 +1755,7 @@
         // Note: at this point no shortcuts should have bitmaps pending save, but if they do,
         // just remove the bitmap.
         if (si.isIconPendingSave()) {
-            s.removeIconLocked(si);
+            removeIcon(si);
         }
         out.startTag(null, TAG_SHORTCUT);
         ShortcutService.writeAttr(out, ATTR_ID, si.getId());
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 6e0436f..7800183 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -16,8 +16,10 @@
 package com.android.server.pm;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.TypedXmlSerializer;
@@ -50,6 +52,9 @@
 
     protected ShortcutUser mShortcutUser;
 
+    @GuardedBy("mLock")
+    protected ShortcutBitmapSaver mShortcutBitmapSaver;
+
     protected final Object mLock = new Object();
 
     protected ShortcutPackageItem(@NonNull ShortcutUser shortcutUser,
@@ -59,6 +64,7 @@
         mPackageUserId = packageUserId;
         mPackageName = Preconditions.checkStringNotEmpty(packageName);
         mPackageInfo = Objects.requireNonNull(packageInfo);
+        mShortcutBitmapSaver = new ShortcutBitmapSaver(shortcutUser.mService);
     }
 
     /**
@@ -206,7 +212,7 @@
 
     void saveShortcutPackageItem() {
         // Wait for bitmap saves to conclude before proceeding to saving shortcuts.
-        mShortcutUser.mService.waitForBitmapSaves();
+        waitForBitmapSaves();
         // Save each ShortcutPackageItem in a separate Xml file.
         final File path = getShortcutPackageItemFile();
         if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) {
@@ -221,6 +227,35 @@
         }
     }
 
+    public boolean waitForBitmapSaves() {
+        synchronized (mLock) {
+            return mShortcutBitmapSaver.waitForAllSavesLocked();
+        }
+    }
+
+    public void saveBitmap(ShortcutInfo shortcut,
+            int maxDimension, Bitmap.CompressFormat format, int quality) {
+        synchronized (mLock) {
+            mShortcutBitmapSaver.saveBitmapLocked(shortcut, maxDimension, format, quality);
+        }
+    }
+
+    /**
+     * Wait for all pending saves to finish, and then return the given shortcut's bitmap path.
+     */
+    @Nullable
+    public String getBitmapPathMayWait(ShortcutInfo shortcut) {
+        synchronized (mLock) {
+            return mShortcutBitmapSaver.getBitmapPathMayWaitLocked(shortcut);
+        }
+    }
+
+    public void removeIcon(ShortcutInfo shortcut) {
+        synchronized (mLock) {
+            mShortcutBitmapSaver.removeIcon(shortcut);
+        }
+    }
+
     void removeShortcutPackageItem() {
         synchronized (mLock) {
             getShortcutPackageItemFile().delete();
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 780f976..f2bcf5e 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -363,7 +363,6 @@
     private final RoleManager mRoleManager;
 
     private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor;
-    private final ShortcutBitmapSaver mShortcutBitmapSaver;
     private final ShortcutDumpFiles mShortcutDumpFiles;
 
     @GuardedBy("mLock")
@@ -490,7 +489,6 @@
         mRoleManager = Objects.requireNonNull(mContext.getSystemService(RoleManager.class));
 
         mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mLock);
-        mShortcutBitmapSaver = new ShortcutBitmapSaver(this);
         mShortcutDumpFiles = new ShortcutDumpFiles(this);
         mIsAppSearchEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.SHORTCUT_APPSEARCH_INTEGRATION, true)
@@ -1063,8 +1061,6 @@
             Slog.d(TAG, "Saving to " + path);
         }
 
-        mShortcutBitmapSaver.waitForAllSavesLocked();
-
         path.getParentFile().mkdirs();
         final AtomicFile file = new AtomicFile(path);
         FileOutputStream os = null;
@@ -1388,15 +1384,12 @@
 
     // === Caller validation ===
 
-    void removeIconLocked(ShortcutInfo shortcut) {
-        mShortcutBitmapSaver.removeIcon(shortcut);
-    }
-
     public void cleanupBitmapsForPackage(@UserIdInt int userId, String packageName) {
         final File packagePath = new File(getUserBitmapFilePath(userId), packageName);
         if (!packagePath.isDirectory()) {
             return;
         }
+        // ShortcutPackage is already removed at this point, we can safely remove the folder.
         if (!(FileUtils.deleteContents(packagePath) && packagePath.delete())) {
             Slog.w(TAG, "Unable to remove directory " + packagePath);
         }
@@ -1437,38 +1430,12 @@
                 }
                 cleanupBitmapsForPackage(userId, packageName);
             } else {
-                cleanupDanglingBitmapFilesLocked(userId, user, packageName, child);
+                user.getPackageShortcuts(packageName).cleanupDanglingBitmapFiles(child);
             }
         }
         logDurationStat(Stats.CLEANUP_DANGLING_BITMAPS, start);
     }
 
-    /**
-     * Remove dangling bitmap files for a package.
-     *
-     * Note this method must be called with the lock held after calling
-     * {@link ShortcutBitmapSaver#waitForAllSavesLocked()} to make sure there's no pending bitmap
-     * saves are going on.
-     */
-    private void cleanupDanglingBitmapFilesLocked(@UserIdInt int userId, @NonNull ShortcutUser user,
-            @NonNull String packageName, @NonNull File path) {
-        final ArraySet<String> usedFiles =
-                user.getPackageShortcuts(packageName).getUsedBitmapFiles();
-
-        for (File child : path.listFiles()) {
-            if (!child.isFile()) {
-                continue;
-            }
-            final String name = child.getName();
-            if (!usedFiles.contains(name)) {
-                if (DEBUG) {
-                    Slog.d(TAG, "Removing dangling bitmap file: " + child.getAbsolutePath());
-                }
-                child.delete();
-            }
-        }
-    }
-
     @VisibleForTesting
     static class FileOutputStreamWithPath extends FileOutputStream {
         private final File mFile;
@@ -1513,7 +1480,7 @@
         }
     }
 
-    void saveIconAndFixUpShortcutLocked(ShortcutInfo shortcut) {
+    void saveIconAndFixUpShortcutLocked(ShortcutPackage p, ShortcutInfo shortcut) {
         if (shortcut.hasIconFile() || shortcut.hasIconResource() || shortcut.hasIconUri()) {
             return;
         }
@@ -1521,7 +1488,7 @@
         final long token = injectClearCallingIdentity();
         try {
             // Clear icon info on the shortcut.
-            removeIconLocked(shortcut);
+            p.removeIcon(shortcut);
 
             final Icon icon = shortcut.getIcon();
             if (icon == null) {
@@ -1560,8 +1527,7 @@
                         // just in case.
                         throw ShortcutInfo.getInvalidIconException();
                 }
-                mShortcutBitmapSaver.saveBitmapLocked(shortcut,
-                        maxIconDimension, mIconPersistFormat, mIconPersistQuality);
+                p.saveBitmap(shortcut, maxIconDimension, mIconPersistFormat, mIconPersistQuality);
             } finally {
                 // Once saved, we won't use the original icon information, so null it out.
                 shortcut.clearIcon();
@@ -2110,7 +2076,7 @@
 
                     final boolean replacingIcon = (source.getIcon() != null);
                     if (replacingIcon) {
-                        removeIconLocked(target);
+                        ps.removeIcon(target);
                     }
 
                     // Note copyNonNullFieldsFrom() does the "updatable with?" check too.
@@ -2118,7 +2084,7 @@
                     target.setTimestamp(injectCurrentTimeMillis());
 
                     if (replacingIcon) {
-                        saveIconAndFixUpShortcutLocked(target);
+                        saveIconAndFixUpShortcutLocked(ps, target);
                     }
 
                     // When we're updating any resource related fields, re-extract the res
@@ -3463,7 +3429,7 @@
                 if (shortcutInfo == null) {
                     return null;
                 }
-                return getShortcutIconParcelFileDescriptor(shortcutInfo);
+                return getShortcutIconParcelFileDescriptor(p, shortcutInfo);
             }
         }
 
@@ -3476,6 +3442,7 @@
             Objects.requireNonNull(shortcutId, "shortcutId");
 
             // Checks shortcuts in memory first
+            final ShortcutPackage p;
             synchronized (mLock) {
                 throwIfUserLockedL(userId);
                 throwIfUserLockedL(launcherUserId);
@@ -3483,8 +3450,7 @@
                 getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
                         .attemptToRestoreIfNeededAndSave();
 
-                final ShortcutPackage p = getUserShortcutsLocked(userId)
-                        .getPackageShortcutsIfExists(packageName);
+                p = getUserShortcutsLocked(userId).getPackageShortcutsIfExists(packageName);
                 if (p == null) {
                     cb.complete(null);
                     return;
@@ -3492,24 +3458,23 @@
 
                 final ShortcutInfo shortcutInfo = p.findShortcutById(shortcutId);
                 if (shortcutInfo != null) {
-                    cb.complete(getShortcutIconParcelFileDescriptor(shortcutInfo));
+                    cb.complete(getShortcutIconParcelFileDescriptor(p, shortcutInfo));
                     return;
                 }
             }
 
             // Otherwise check persisted shortcuts
-            getShortcutInfoAsync(launcherUserId, packageName, shortcutId, userId, si -> {
-                cb.complete(getShortcutIconParcelFileDescriptor(si));
-            });
+            getShortcutInfoAsync(launcherUserId, packageName, shortcutId, userId, si ->
+                    cb.complete(getShortcutIconParcelFileDescriptor(p, si)));
         }
 
         @Nullable
         private ParcelFileDescriptor getShortcutIconParcelFileDescriptor(
-                @Nullable final ShortcutInfo shortcutInfo) {
-            if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
+                @Nullable final ShortcutPackage p, @Nullable final ShortcutInfo shortcutInfo) {
+            if (p == null || shortcutInfo == null || !shortcutInfo.hasIconFile()) {
                 return null;
             }
-            final String path = mShortcutBitmapSaver.getBitmapPathMayWaitLocked(shortcutInfo);
+            final String path = p.getBitmapPathMayWait(shortcutInfo);
             if (path == null) {
                 Slog.w(TAG, "null bitmap detected in getShortcutIconFd()");
                 return null;
@@ -4772,9 +4737,6 @@
                 }
 
                 pw.println();
-                mShortcutBitmapSaver.dumpLocked(pw, "  ");
-
-                pw.println();
             }
 
             for (int i = 0; i < mUsers.size(); i++) {
@@ -5347,9 +5309,11 @@
         }
     }
 
-    void waitForBitmapSaves() {
+    @VisibleForTesting
+    void waitForBitmapSavesForTest() {
         synchronized (mLock) {
-            mShortcutBitmapSaver.waitForAllSavesLocked();
+            forEachLoadedUserLocked(u ->
+                    u.forAllPackageItems(ShortcutPackageItem::waitForBitmapSaves));
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index 75e18b5..b9fd2fd 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -401,6 +401,7 @@
 
     private void saveShortcutPackageItem(TypedXmlSerializer out, ShortcutPackageItem spi,
             boolean forBackup) throws IOException, XmlPullParserException {
+        spi.waitForBitmapSaves();
         if (forBackup) {
             if (spi.getPackageUserId() != spi.getOwnerUserId()) {
                 return; // Don't save cross-user information.
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 0dabff8..bcdf429 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1430,6 +1430,8 @@
     /**
      * Returns a UserInfo object with the name filled in, for Owner and Guest, or the original
      * if the name is already set.
+     *
+     * Note: Currently, the resulting name can be null if a user was truly created with a null name.
      */
     private UserInfo userWithName(UserInfo orig) {
         if (orig != null && orig.name == null) {
@@ -1638,7 +1640,7 @@
     }
 
     @Override
-    public String getUserName() {
+    public @NonNull String getUserName() {
         final int callingUid = Binder.getCallingUid();
         if (!hasQueryOrCreateUsersPermission()
                 && !hasPermissionGranted(
@@ -1649,7 +1651,10 @@
         final int userId = UserHandle.getUserId(callingUid);
         synchronized (mUsersLock) {
             UserInfo userInfo = userWithName(getUserInfoLU(userId));
-            return userInfo == null ? "" : userInfo.name;
+            if (userInfo != null && userInfo.name != null) {
+                return userInfo.name;
+            }
+            return "";
         }
     }
 
@@ -4165,7 +4170,7 @@
      * @return the converted user, or {@code null} if no pre-created user could be converted.
      */
     private @Nullable UserInfo convertPreCreatedUserIfPossible(String userType,
-            @UserInfoFlag int flags, String name, @Nullable Object token) {
+            @UserInfoFlag int flags, @Nullable String name, @Nullable Object token) {
         final UserData preCreatedUserData;
         synchronized (mUsersLock) {
             preCreatedUserData = getPreCreatedUserLU(userType);
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 2277d8a..7be83b0 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -2816,14 +2816,12 @@
                             }
 
                             // Remove review flag as it is not necessary anymore
-                            // TODO(b/227186603) re-enable check for notification permission once
-                            // droidfood state has been cleared
-                            //if (!NOTIFICATION_PERMISSIONS.contains(perm)) {
+                            if (!NOTIFICATION_PERMISSIONS.contains(perm)) {
                                 if ((flags & FLAG_PERMISSION_REVIEW_REQUIRED) != 0) {
                                     flags &= ~FLAG_PERMISSION_REVIEW_REQUIRED;
                                     wasChanged = true;
                                 }
-                            //}
+                            }
 
                             if ((flags & FLAG_PERMISSION_REVOKED_COMPAT) != 0
                                     && !isPermissionSplitFromNonRuntime(permName,
diff --git a/services/core/java/com/android/server/power/LowPowerStandbyController.java b/services/core/java/com/android/server/power/LowPowerStandbyController.java
index 2d2bad2..5964fa4 100644
--- a/services/core/java/com/android/server/power/LowPowerStandbyController.java
+++ b/services/core/java/com/android/server/power/LowPowerStandbyController.java
@@ -68,7 +68,7 @@
  *
  * @hide
  */
-public final class LowPowerStandbyController {
+public class LowPowerStandbyController {
     private static final String TAG = "LowPowerStandbyController";
     private static final boolean DEBUG = false;
     private static final boolean DEFAULT_ACTIVE_DURING_MAINTENANCE = false;
@@ -173,7 +173,9 @@
         mSettingsObserver = new SettingsObserver(mHandler);
     }
 
-    void systemReady() {
+    /** Call when system services are ready */
+    @VisibleForTesting
+    public void systemReady() {
         final Resources resources = mContext.getResources();
         synchronized (mLock) {
             mSupportedConfig = resources.getBoolean(
@@ -435,7 +437,9 @@
         }
     }
 
-    void setActiveDuringMaintenance(boolean activeDuringMaintenance) {
+    /** Set whether Low Power Standby should be active during doze maintenance mode. */
+    @VisibleForTesting
+    public void setActiveDuringMaintenance(boolean activeDuringMaintenance) {
         synchronized (mLock) {
             if (!mSupportedConfig) {
                 Slog.w(TAG, "Low Power Standby settings cannot be changed "
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 0834417..3c779f3 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -727,7 +727,8 @@
                 return false;
             }
 
-            if (mKeyguardManager != null && mKeyguardManager.isDeviceLocked(userId)) {
+            if (requiresAuthentication() && mKeyguardManager != null
+                    && mKeyguardManager.isDeviceLocked(userId)) {
                 Log.i(TAG, "Can't change mic/cam toggle while device is locked");
                 return false;
             }
@@ -993,6 +994,13 @@
         }
 
         @Override
+        public boolean requiresAuthentication() {
+            enforceObserveSensorPrivacyPermission();
+            return mContext.getResources()
+                    .getBoolean(R.bool.config_sensorPrivacyRequiresAuthentication);
+        }
+
+        @Override
         public void showSensorUseDialog(int sensor) {
             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
                 throw new SecurityException("Can only be called by the system uid");
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index b685572..d48f263 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1650,13 +1650,11 @@
     }
 
     @Override
-    public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed,
-            boolean isBubbleSuppressed) {
+    public void onBubbleMetadataFlagChanged(String key, int flags) {
         enforceStatusBarService();
         final long identity = Binder.clearCallingIdentity();
         try {
-            mNotificationDelegate.onBubbleNotificationSuppressionChanged(key, isNotifSuppressed,
-                    isBubbleSuppressed);
+            mNotificationDelegate.onBubbleMetadataFlagChanged(key, flags);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 732cb71..791d193 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -5096,7 +5096,11 @@
         final boolean recentsAnimating = isAnimating(PARENTS, ANIMATION_TYPE_RECENTS);
         if (okToAnimate(true /* ignoreFrozen */, canTurnScreenOn())
                 && (appTransition.isTransitionSet()
-                || (recentsAnimating && !isActivityTypeHome()))) {
+                || (recentsAnimating && !isActivityTypeHome()))
+                // If the visibility change during enter PIP, we don't want to include it in app
+                // transition to affect the animation theme, because the Pip organizer will animate
+                // the entering PIP instead.
+                && !mWaitForEnteringPinnedMode) {
             if (visible) {
                 displayContent.mOpeningApps.add(this);
                 mEnteringAnimation = true;
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 34c083a..b97ee7e 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1807,6 +1807,10 @@
         // Check if starting activity on given task or on a new task is allowed.
         int startResult = isAllowedToStart(r, newTask, targetTask);
         if (startResult != START_SUCCESS) {
+            if (r.resultTo != null) {
+                r.resultTo.sendResult(INVALID_UID, r.resultWho, r.requestCode, RESULT_CANCELED,
+                        null /* data */, null /* dataGrants */);
+            }
             return startResult;
         }
 
@@ -1976,13 +1980,9 @@
         mPreferredWindowingMode = mLaunchParams.mWindowingMode;
     }
 
-    private int isAllowedToStart(ActivityRecord r, boolean newTask, Task targetTask) {
-        if (mStartActivity.packageName == null) {
-            if (mStartActivity.resultTo != null) {
-                mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho,
-                        mStartActivity.requestCode, RESULT_CANCELED,
-                        null /* data */, null /* dataGrants */);
-            }
+    @VisibleForTesting
+    int isAllowedToStart(ActivityRecord r, boolean newTask, Task targetTask) {
+        if (r.packageName == null) {
             ActivityOptions.abort(mOptions);
             return START_CLASS_NOT_FOUND;
         }
@@ -2005,8 +2005,7 @@
                 || !targetTask.isUidPresent(mCallingUid)
                 || (LAUNCH_SINGLE_INSTANCE == mLaunchMode && targetTask.inPinnedWindowingMode()));
 
-        if (mRestrictedBgActivity && blockBalInTask
-                && handleBackgroundActivityAbort(mStartActivity)) {
+        if (mRestrictedBgActivity && blockBalInTask && handleBackgroundActivityAbort(r)) {
             Slog.e(TAG, "Abort background activity starts from " + mCallingUid);
             return START_ABORTED;
         }
@@ -2020,12 +2019,12 @@
         if (!newTask) {
             if (mService.getLockTaskController().isLockTaskModeViolation(targetTask,
                     isNewClearTask)) {
-                Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
+                Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r);
                 return START_RETURN_LOCK_TASK_MODE_VIOLATION;
             }
         } else {
-            if (mService.getLockTaskController().isNewTaskLockTaskModeViolation(mStartActivity)) {
-                Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
+            if (mService.getLockTaskController().isNewTaskLockTaskModeViolation(r)) {
+                Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r);
                 return START_RETURN_LOCK_TASK_MODE_VIOLATION;
             }
         }
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 701fc94..cb65597 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -68,6 +68,7 @@
 import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldAttachNavBarToApp;
 import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldStartNonAppWindowAnimationsForKeyguardExit;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
@@ -1142,13 +1143,17 @@
             if (activity == null) {
                 continue;
             }
+            if (activity.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
+                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
+                        "Delaying app transition for recents animation to finish");
+                return false;
+            }
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                     "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
                             + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
                     activity, activity.allDrawn, activity.startingDisplayed,
                     activity.startingMoved, activity.isRelaunching(),
                     activity.mStartingWindow);
-
             final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
             if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
                 return false;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ed1bbf8..2975a95 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -4272,7 +4272,15 @@
             setImeInputTarget(target);
             mInsetsStateController.updateAboveInsetsState(mInsetsStateController
                     .getRawInsetsState().getSourceOrDefaultVisibility(ITYPE_IME));
-            updateImeControlTarget();
+            // Force updating the IME parent when the IME control target has been updated to the
+            // remote target but updateImeParent not happen because ImeLayeringTarget and
+            // ImeInputTarget are different. Then later updateImeParent would be ignored when there
+            // is no new IME control target to change the IME parent.
+            final boolean forceUpdateImeParent = mImeControlTarget == mRemoteInsetsControlTarget
+                    && (mInputMethodSurfaceParent != null
+                    && !mInputMethodSurfaceParent.isSameSurface(
+                            mImeWindowsContainer.getParent().mSurfaceControl));
+            updateImeControlTarget(forceUpdateImeParent);
         }
         // Unfreeze IME insets after the new target updated, in case updateAboveInsetsState may
         // deliver unrelated IME insets change to the non-IME requester.
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 67dd89e..33cdd2e 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -270,6 +270,22 @@
         }
     }
 
+    @Override
+    public void notifyPointerDisplayIdChanged(int displayId, float x, float y) {
+        synchronized (mService.mGlobalLock) {
+            mService.setMousePointerDisplayId(displayId);
+            if (displayId == Display.INVALID_DISPLAY) return;
+
+            final DisplayContent dc = mService.mRoot.getDisplayContent(displayId);
+            if (dc == null) {
+                Slog.wtf(TAG, "The mouse pointer was moved to display " + displayId
+                        + " that does not have a valid DisplayContent.");
+                return;
+            }
+            mService.restorePointerIconLocked(dc, x, y);
+        }
+    }
+
     /** Waits until the built-in input devices have been configured. */
     public boolean waitForInputDevicesReady(long timeoutMillis) {
         synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index efe617d..2bae59a 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -19,7 +19,6 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
@@ -602,22 +601,12 @@
                 || mDisplayContent.getAsyncRotationController() != null) {
             return;
         }
-        boolean shouldTranslateNavBar = false;
-        final boolean isDisplayLandscape =
-                mDisplayContent.getConfiguration().orientation == ORIENTATION_LANDSCAPE;
         for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
             final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
             final Task task = adapter.mTask;
-            final TaskFragment adjacentTask = task.getRootTask().getAdjacentTaskFragment();
-            final boolean inSplitScreen = task.inSplitScreen();
-            if (task.isActivityTypeHomeOrRecents()
-                    // Skip if the task is in split screen and in landscape.
-                    || (inSplitScreen && isDisplayLandscape)
-                    // Skip if the task is the top task in split screen.
-                    || (inSplitScreen && task.getBounds().top < adjacentTask.getBounds().top)) {
+            if (task.isActivityTypeHomeOrRecents()) {
                 continue;
             }
-            shouldTranslateNavBar = inSplitScreen;
             mNavBarAttachedApp = task.getTopVisibleActivity();
             break;
         }
@@ -630,9 +619,7 @@
         navWindow.mToken.cancelAnimation();
         final SurfaceControl.Transaction t = navWindow.mToken.getPendingTransaction();
         final SurfaceControl navSurfaceControl = navWindow.mToken.getSurfaceControl();
-        if (shouldTranslateNavBar) {
-            navWindow.setSurfaceTranslationY(-mNavBarAttachedApp.getBounds().top);
-        }
+        navWindow.setSurfaceTranslationY(-mNavBarAttachedApp.getBounds().top);
         t.reparent(navSurfaceControl, mNavBarAttachedApp.getSurfaceControl());
         t.show(navSurfaceControl);
 
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
index 83be73a..4068a97 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.util.TimeUtils.NANOS_PER_MS;
 import static android.view.Choreographer.CALLBACK_TRAVERSAL;
 import static android.view.Choreographer.getSfInstance;
@@ -26,14 +27,13 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.Nullable;
-import android.graphics.Canvas;
 import android.graphics.Insets;
-import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.hardware.power.Boost;
 import android.os.Handler;
 import android.os.PowerManagerInternal;
+import android.os.Trace;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.Choreographer;
@@ -50,6 +50,8 @@
 import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
 
 import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.function.Supplier;
 
 /**
@@ -83,6 +85,12 @@
     private final PowerManagerInternal mPowerManagerInternal;
     private boolean mApplyScheduled;
 
+    // Executor to perform the edge extension.
+    // With two threads because in practice we will want to extend two surfaces in one animation,
+    // in which case we want to be able to parallelize those two extensions to cut down latency in
+    // starting the animation.
+    private final ExecutorService mEdgeExtensionExecutor = Executors.newFixedThreadPool(2);
+
     @GuardedBy("mLock")
     @VisibleForTesting
     final ArrayMap<SurfaceControl, RunningAnimation> mPendingAnimations = new ArrayMap<>();
@@ -173,7 +181,7 @@
 
                 // We must wait for t to be committed since otherwise the leash doesn't have the
                 // windows we want to screenshot and extend as children.
-                t.addTransactionCommittedListener(Runnable::run, () -> {
+                t.addTransactionCommittedListener(mEdgeExtensionExecutor, () -> {
                     final WindowAnimationSpec animationSpec = a.asWindowAnimationSpec();
 
                     final Transaction edgeExtensionCreationTransaction = new Transaction();
@@ -403,30 +411,17 @@
     private void createExtensionSurface(SurfaceControl leash, Rect edgeBounds,
             Rect extensionRect, int xPos, int yPos, String layerName,
             Transaction startTransaction) {
-        synchronized (mEdgeExtensionLock) {
-            if (!mEdgeExtensions.containsKey(leash)) {
-                // Animation leash has already been removed so we shouldn't perform any extension
-                return;
-            }
-            createExtensionSurfaceLocked(leash, edgeBounds, extensionRect, xPos, yPos, layerName,
-                    startTransaction);
-        }
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "createExtensionSurface");
+        doCreateExtensionSurface(leash, edgeBounds, extensionRect, xPos, yPos, layerName,
+                startTransaction);
+        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
     }
 
-    private void createExtensionSurfaceLocked(SurfaceControl surfaceToExtend, Rect edgeBounds,
+    private void doCreateExtensionSurface(SurfaceControl leash, Rect edgeBounds,
             Rect extensionRect, int xPos, int yPos, String layerName,
             Transaction startTransaction) {
-        final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
-                .setName(layerName)
-                .setParent(surfaceToExtend)
-                .setHidden(true)
-                .setCallsite("DefaultTransitionHandler#startAnimation")
-                .setOpaque(true)
-                .setBufferSize(extensionRect.width(), extensionRect.height())
-                .build();
-
         SurfaceControl.LayerCaptureArgs captureArgs =
-                new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend)
+                new SurfaceControl.LayerCaptureArgs.Builder(leash /* surfaceToExtend */)
                         .setSourceCrop(edgeBounds)
                         .setFrameScale(1)
                         .setPixelFormat(PixelFormat.RGBA_8888)
@@ -437,31 +432,76 @@
                 SurfaceControl.captureLayers(captureArgs);
 
         if (edgeBuffer == null) {
+            // The leash we are trying to screenshot may have been removed by this point, which is
+            // likely the reason for ending up with a null edgeBuffer, in which case we just want to
+            // return and do nothing.
             Log.e("SurfaceAnimationRunner", "Failed to create edge extension - "
                     + "edge buffer is null");
             return;
         }
 
-        android.graphics.BitmapShader shader =
-                new android.graphics.BitmapShader(edgeBuffer.asBitmap(),
-                        android.graphics.Shader.TileMode.CLAMP,
-                        android.graphics.Shader.TileMode.CLAMP);
-        final Paint paint = new Paint();
-        paint.setShader(shader);
+        final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
+                .setName(layerName)
+                .setHidden(true)
+                .setCallsite("DefaultTransitionHandler#startAnimation")
+                .setOpaque(true)
+                .setBufferSize(edgeBounds.width(), edgeBounds.height())
+                .build();
 
         final Surface surface = new Surface(edgeExtensionLayer);
-        Canvas c = surface.lockHardwareCanvas();
-        c.drawRect(extensionRect, paint);
-        surface.unlockCanvasAndPost(c);
+        surface.attachAndQueueBufferWithColorSpace(edgeBuffer.getHardwareBuffer(),
+                edgeBuffer.getColorSpace());
         surface.release();
 
-        startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
-        startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
-        startTransaction.setVisibility(edgeExtensionLayer, true);
+        final float scaleX = getScaleXForExtensionSurface(edgeBounds, extensionRect);
+        final float scaleY = getScaleYForExtensionSurface(edgeBounds, extensionRect);
 
-        mEdgeExtensions.get(surfaceToExtend).add(edgeExtensionLayer);
+        synchronized (mEdgeExtensionLock) {
+            if (!mEdgeExtensions.containsKey(leash)) {
+                // The animation leash has already been removed, so we don't want to attach the
+                // edgeExtension layer and should immediately remove it instead.
+                startTransaction.remove(edgeExtensionLayer);
+                return;
+            }
+
+            startTransaction.setScale(edgeExtensionLayer, scaleX, scaleY);
+            startTransaction.reparent(edgeExtensionLayer, leash);
+            startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
+            startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
+            startTransaction.setVisibility(edgeExtensionLayer, true);
+
+            mEdgeExtensions.get(leash).add(edgeExtensionLayer);
+        }
     }
 
+    private float getScaleXForExtensionSurface(Rect edgeBounds, Rect extensionRect) {
+        if (edgeBounds.width() == extensionRect.width()) {
+            // Top or bottom edge extension, no need to scale the X axis of the extension surface.
+            return 1;
+        }
+        if (edgeBounds.width() == 1) {
+            // Left or right edge extension, scale the surface to be the extensionRect's width.
+            return extensionRect.width();
+        }
+
+        throw new RuntimeException("Unexpected edgeBounds and extensionRect widths");
+    }
+
+    private float getScaleYForExtensionSurface(Rect edgeBounds, Rect extensionRect) {
+        if (edgeBounds.height() == extensionRect.height()) {
+            // Left or right edge extension, no need to scale the Y axis of the extension surface.
+            return 1;
+        }
+        if (edgeBounds.height() == 1) {
+            // Top or bottom edge extension, scale the surface to be the extensionRect's height.
+            return extensionRect.height();
+        }
+
+        throw new RuntimeException("Unexpected edgeBounds and extensionRect heights");
+    }
+
+
+
     private static final class RunningAnimation {
         final AnimationSpec mAnimSpec;
         final SurfaceControl mLeash;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 718ce28..60c280c 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -28,7 +28,6 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.activityTypeToString;
@@ -1720,13 +1719,6 @@
                 && (topTask == null || topTask.supportsSplitScreenWindowingModeInner(tda));
     }
 
-    /** Returns {@code true} if this task is currently in split-screen. */
-    boolean inSplitScreen() {
-        return getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
-                && getCreatedByOrganizerTask() != null
-                && getCreatedByOrganizerTask().getAdjacentTaskFragment() != null;
-    }
-
     private boolean supportsSplitScreenWindowingModeInner(@Nullable TaskDisplayArea tda) {
         return super.supportsSplitScreenWindowingMode()
                 && mAtmService.mSupportsSplitScreenMultiWindow
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 1176182..ce406e4 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1163,20 +1163,21 @@
             }
         }
 
-        // For a better split UX, If a task is launching from a created-by-organizer task, it should
-        // be launched into the same created-by-organizer task as well. Unless, the candidate task
-        // is already positioned in the split.
-        Task preferredRootInSplit = sourceTask != null && sourceTask.inSplitScreen()
-                ? sourceTask.getCreatedByOrganizerTask() : null;
-        if (preferredRootInSplit != null) {
-            if (candidateTask != null) {
-                final Task candidateRoot = candidateTask.getCreatedByOrganizerTask();
-                if (candidateRoot != null && candidateRoot != preferredRootInSplit
-                        && preferredRootInSplit == candidateRoot.getAdjacentTaskFragment()) {
-                    preferredRootInSplit = candidateRoot;
+        // If a task is launching from a created-by-organizer task, it should be launched into the
+        // same created-by-organizer task as well. Unless, the candidate task is already positioned
+        // in the another adjacent task.
+        if (sourceTask != null) {
+            Task launchTarget = sourceTask.getCreatedByOrganizerTask();
+            if (launchTarget != null && launchTarget.getAdjacentTaskFragment() != null) {
+                if (candidateTask != null) {
+                    final Task candidateRoot = candidateTask.getCreatedByOrganizerTask();
+                    if (candidateRoot != null && candidateRoot != launchTarget
+                            && launchTarget == candidateRoot.getAdjacentTaskFragment()) {
+                        launchTarget = candidateRoot;
+                    }
                 }
+                return launchTarget;
             }
-            return preferredRootInSplit;
         }
 
         return null;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index c7bc513..a480c37 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1584,6 +1584,14 @@
         return true;
     }
 
+    void forAllWindowContainers(Consumer<WindowContainer> callback) {
+        callback.accept(this);
+        final int count = mChildren.size();
+        for (int i = 0; i < count; i++) {
+            mChildren.get(i).forAllWindowContainers(callback);
+        }
+    }
+
     /**
      * For all windows at or below this container call the callback.
      * @param   callback Calls the {@link ToBooleanFunction#apply} method for each window found and
@@ -3741,8 +3749,16 @@
             }
             // Otherwise this is the "root" of a synced subtree, so continue on to preparation.
         }
+
         // This container's situation has changed so we need to restart its sync.
-        mSyncState = SYNC_STATE_NONE;
+        // We cannot reset the sync without a chance of a deadlock since it will request a new
+        // buffer from the app process. This could cause issues if the app has run out of buffers
+        // since the previous buffer was already synced and is still held in a transaction.
+        // Resetting syncState violates the policies outlined in BlastSyncEngine.md so for now
+        // disable this when shell transitions is disabled.
+        if (mTransitionController.isShellTransitionsEnabled()) {
+            mSyncState = SYNC_STATE_NONE;
+        }
         prepareSync();
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 7b77fd0..3906a19 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -118,6 +118,7 @@
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG;
@@ -3050,13 +3051,22 @@
         }
     }
 
+
     void cleanupRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode) {
         if (mRecentsAnimationController != null) {
             final RecentsAnimationController controller = mRecentsAnimationController;
             mRecentsAnimationController = null;
             controller.cleanupAnimation(reorderMode);
-            // TODO(mult-display): currently only default display support recents animation.
-            getDefaultDisplayContentLocked().mAppTransition.updateBooster();
+            // TODO(multi-display): currently only default display support recents animation.
+            // Cancel any existing app transition animation running in the legacy transition
+            // framework.
+            final DisplayContent dc = getDefaultDisplayContentLocked();
+            dc.mAppTransition.freeze();
+            dc.forAllWindowContainers((wc) -> {
+                if (wc.isAnimating(TRANSITION, ANIMATION_TYPE_APP_TRANSITION)) {
+                    wc.cancelAnimation();
+                }
+            });
         }
     }
 
@@ -7198,18 +7208,42 @@
         private float mLatestMouseX;
         private float mLatestMouseY;
 
-        void updatePosition(float x, float y) {
+        /**
+         * The display that the pointer (mouse cursor) is currently shown on. This is updated
+         * directly by InputManagerService when the pointer display changes.
+         */
+        private int mPointerDisplayId = INVALID_DISPLAY;
+
+        /**
+         * Update the mouse cursor position as a result of a mouse movement.
+         * @return true if the position was successfully updated, false otherwise.
+         */
+        boolean updatePosition(int displayId, float x, float y) {
             synchronized (this) {
                 mLatestEventWasMouse = true;
+
+                if (displayId != mPointerDisplayId) {
+                    // The display of the position update does not match the display on which the
+                    // mouse pointer is shown, so do not update the position.
+                    return false;
+                }
                 mLatestMouseX = x;
                 mLatestMouseY = y;
+                return true;
+            }
+        }
+
+        void setPointerDisplayId(int displayId) {
+            synchronized (this) {
+                mPointerDisplayId = displayId;
             }
         }
 
         @Override
         public void onPointerEvent(MotionEvent motionEvent) {
             if (motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
-                updatePosition(motionEvent.getRawX(), motionEvent.getRawY());
+                updatePosition(motionEvent.getDisplayId(), motionEvent.getRawX(),
+                        motionEvent.getRawY());
             } else {
                 synchronized (this) {
                     mLatestEventWasMouse = false;
@@ -7219,6 +7253,7 @@
     };
 
     void updatePointerIcon(IWindow client) {
+        int pointerDisplayId;
         float mouseX, mouseY;
 
         synchronized(mMousePositionTracker) {
@@ -7227,6 +7262,7 @@
             }
             mouseX = mMousePositionTracker.mLatestMouseX;
             mouseY = mMousePositionTracker.mLatestMouseY;
+            pointerDisplayId = mMousePositionTracker.mPointerDisplayId;
         }
 
         synchronized (mGlobalLock) {
@@ -7243,6 +7279,10 @@
             if (displayContent == null) {
                 return;
             }
+            if (pointerDisplayId != displayContent.getDisplayId()) {
+                // Do not let the pointer icon be updated by a window on a different display.
+                return;
+            }
             WindowState windowUnderPointer =
                     displayContent.getTouchableWinAtPointLocked(mouseX, mouseY);
             if (windowUnderPointer != callingWin) {
@@ -7260,7 +7300,11 @@
 
     void restorePointerIconLocked(DisplayContent displayContent, float latestX, float latestY) {
         // Mouse position tracker has not been getting updates while dragging, update it now.
-        mMousePositionTracker.updatePosition(latestX, latestY);
+        if (!mMousePositionTracker.updatePosition(
+                displayContent.getDisplayId(), latestX, latestY)) {
+            // The mouse position could not be updated, so ignore this request.
+            return;
+        }
 
         WindowState windowUnderPointer =
                 displayContent.getTouchableWinAtPointLocked(latestX, latestY);
@@ -7284,6 +7328,10 @@
         }
     }
 
+    void setMousePointerDisplayId(int displayId) {
+        mMousePositionTracker.setPointerDisplayId(displayId);
+    }
+
     /**
      * Update a tap exclude region in the window identified by the provided id. Touches down on this
      * region will not:
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index cd19f64..3b282aa 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3639,13 +3639,11 @@
         final int requested = mLastRequestedExclusionHeight[side];
         final int granted = mLastGrantedExclusionHeight[side];
 
-        final boolean inSplitScreen = getTask() != null && getTask().inSplitScreen();
-
         FrameworkStatsLog.write(FrameworkStatsLog.EXCLUSION_RECT_STATE_CHANGED,
                 mAttrs.packageName, requested, requested - granted /* rejected */,
                 side + 1 /* Sides are 1-indexed in atoms.proto */,
                 (getConfiguration().orientation == ORIENTATION_LANDSCAPE),
-                inSplitScreen, (int) duration);
+                false /* (deprecated param) inSplitscreen */, (int) duration);
     }
 
     private void initExclusionRestrictions() {
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 49a4021..93152f2 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -66,13 +66,13 @@
 // Defines the maximum amount of VMAs we can send per process_madvise syscall.
 // Currently this is set to UIO_MAXIOV which is the maximum segments allowed by
 // iovec implementation used by process_madvise syscall
-#define MAX_VMAS_PER_BATCH UIO_MAXIOV
+#define MAX_VMAS_PER_COMPACTION UIO_MAXIOV
 
 // Maximum bytes that we can send per process_madvise syscall once this limit
 // is reached we split the remaining VMAs into another syscall. The MAX_RW_COUNT
 // limit is imposed by iovec implementation. However, if you want to use a smaller
-// limit, it has to be a page aligned value.
-#define MAX_BYTES_PER_BATCH MAX_RW_COUNT
+// limit, it has to be a page aligned value, otherwise, compaction would fail.
+#define MAX_BYTES_PER_COMPACTION MAX_RW_COUNT
 
 // Selected a high enough number to avoid clashing with linux errno codes
 #define ERROR_COMPACTION_CANCELLED -1000
@@ -83,181 +83,6 @@
 // before starting next VMA batch
 static std::atomic<bool> cancelRunningCompaction;
 
-// A VmaBatch represents a set of VMAs that can be processed
-// as VMAs are processed by client code it is expected that the
-// VMAs get consumed which means they are discarded as they are
-// processed so that the first element always is the next element
-// to be sent
-struct VmaBatch {
-    struct iovec* vmas;
-    // total amount of VMAs to reach the end of iovec
-    int totalVmas;
-    // total amount of bytes that are remaining within iovec
-    uint64_t totalBytes;
-};
-
-// Advances the iterator by the specified amount of bytes.
-// This is used to remove already processed or no longer
-// needed parts of the batch.
-// Returns total bytes consumed
-int consumeBytes(VmaBatch& batch, uint64_t bytesToConsume) {
-    int index = 0;
-    if (CC_UNLIKELY(bytesToConsume) < 0) {
-        LOG(ERROR) << "Cannot consume negative bytes for VMA batch !";
-        return 0;
-    }
-
-    if (bytesToConsume > batch.totalBytes) {
-        // Avoid consuming more bytes than available
-        bytesToConsume = batch.totalBytes;
-    }
-
-    uint64_t bytesConsumed = 0;
-    while (bytesConsumed < bytesToConsume) {
-        if (CC_UNLIKELY(index >= batch.totalVmas)) {
-            // reach the end of the batch
-            return bytesConsumed;
-        }
-        if (CC_UNLIKELY(bytesConsumed + batch.vmas[index].iov_len > bytesToConsume)) {
-            // this is the whole VMA that will be consumed
-            break;
-        }
-        bytesConsumed += batch.vmas[index].iov_len;
-        batch.totalBytes -= batch.vmas[index].iov_len;
-        --batch.totalVmas;
-        ++index;
-    }
-
-    // Move pointer to consume all the whole VMAs
-    batch.vmas = batch.vmas + index;
-
-    // Consume the rest of the bytes partially at last VMA in batch
-    uint64_t bytesLeftToConsume = bytesToConsume - bytesConsumed;
-    bytesConsumed += bytesLeftToConsume;
-    if (batch.totalVmas > 0) {
-        batch.vmas[0].iov_base = (void*)((uint64_t)batch.vmas[0].iov_base + bytesLeftToConsume);
-    }
-
-    return bytesConsumed;
-}
-
-// given a source of vmas this class will act as a factory
-// of VmaBatch objects and it will allow generating batches
-// until there are no more left in the source vector.
-// Note: the class does not actually modify the given
-// vmas vector, instead it iterates on it until the end.
-class VmaBatchCreator {
-    const std::vector<Vma>* sourceVmas;
-    // This is the destination array where batched VMAs will be stored
-    // it gets encapsulated into a VmaBatch which is the object
-    // meant to be used by client code.
-    struct iovec* destVmas;
-
-    // Parameters to keep track of the iterator on the source vmas
-    int currentIndex_;
-    uint64_t currentOffset_;
-
-public:
-    VmaBatchCreator(const std::vector<Vma>* vmasToBatch, struct iovec* destVmasVec)
-          : sourceVmas(vmasToBatch), destVmas(destVmasVec), currentIndex_(0), currentOffset_(0) {}
-
-    int currentIndex() { return currentIndex_; }
-    uint64_t currentOffset() { return currentOffset_; }
-
-    // Generates a batch and moves the iterator on the source vmas
-    // past the last VMA in the batch.
-    // Returns true on success, false on failure
-    bool createNextBatch(VmaBatch& batch) {
-        if (currentIndex_ >= MAX_VMAS_PER_BATCH && currentIndex_ >= sourceVmas->size()) {
-            return false;
-        }
-
-        const std::vector<Vma>& vmas = *sourceVmas;
-        batch.vmas = destVmas;
-        uint64_t totalBytesInBatch = 0;
-        int indexInBatch = 0;
-
-        // Add VMAs to the batch up until we consumed all the VMAs or
-        // reached any imposed limit of VMAs per batch.
-        while (indexInBatch < MAX_VMAS_PER_BATCH && currentIndex_ < vmas.size()) {
-            uint64_t vmaStart = vmas[currentIndex_].start + currentOffset_;
-            uint64_t vmaSize = vmas[currentIndex_].end - vmaStart;
-            if (CC_UNLIKELY(vmaSize == 0)) {
-                // No more bytes to batch for this VMA, move to next one
-                // this only happens if a batch partially consumed bytes
-                // and offset landed at exactly the end of a vma
-                continue;
-            }
-            batch.vmas[indexInBatch].iov_base = (void*)vmaStart;
-            uint64_t bytesAvailableInBatch = MAX_BYTES_PER_BATCH - totalBytesInBatch;
-
-            if (vmaSize >= bytesAvailableInBatch) {
-                // VMA would exceed the max available bytes in batch
-                // clamp with available bytes and finish batch.
-                vmaSize = bytesAvailableInBatch;
-                currentOffset_ += bytesAvailableInBatch;
-            }
-
-            batch.vmas[indexInBatch].iov_len = vmaSize;
-            totalBytesInBatch += vmaSize;
-
-            ++indexInBatch;
-            if (totalBytesInBatch >= MAX_BYTES_PER_BATCH) {
-                // Reached max bytes quota so this marks
-                // the end of the batch
-                break;
-            }
-
-            // Fully finished current VMA, move to next one
-            currentOffset_ = 0;
-            ++currentIndex_;
-        }
-        // Vmas where fully filled and we are past the last filled index.
-        batch.totalVmas = indexInBatch;
-        batch.totalBytes = totalBytesInBatch;
-        return true;
-    }
-};
-
-// Madvise a set of VMAs given in a batch for a specific process
-// The total number of bytes successfully madvised will be set on
-// outBytesProcessed.
-// Returns 0 on success and standard linux -errno code returned by
-// process_madvise on failure
-int madviseVmasFromBatch(unique_fd& pidfd, VmaBatch& batch, int madviseType,
-                         uint64_t* outBytesProcessed) {
-    if (batch.totalVmas == 0) {
-        // No VMAs in Batch, skip.
-        *outBytesProcessed = 0;
-        return 0;
-    }
-
-    ATRACE_BEGIN(StringPrintf("Madvise %d: %d VMAs", madviseType, batch.totalVmas).c_str());
-    uint64_t bytesProcessedInSend =
-            process_madvise(pidfd, batch.vmas, batch.totalVmas, madviseType, 0);
-    ATRACE_END();
-
-    if (CC_UNLIKELY(bytesProcessedInSend == -1)) {
-        bytesProcessedInSend = 0;
-        if (errno != EINVAL) {
-            // Forward irrecoverable errors and bail out compaction
-            *outBytesProcessed = 0;
-            return -errno;
-        }
-    }
-
-    if (bytesProcessedInSend < batch.totalBytes) {
-        // Did not process all the bytes requested
-        // skip last page which likely failed
-        bytesProcessedInSend += PAGE_SIZE;
-    }
-
-    bytesProcessedInSend = consumeBytes(batch, bytesProcessedInSend);
-
-    *outBytesProcessed = bytesProcessedInSend;
-    return 0;
-}
-
 // Legacy method for compacting processes, any new code should
 // use compactProcess instead.
 static inline void compactProcessProcfs(int pid, const std::string& compactionType) {
@@ -271,6 +96,8 @@
 // If any VMA fails compaction due to -EINVAL it will be skipped and continue.
 // However, if it fails for any other reason, it will bail out and forward the error
 static int64_t compactMemory(const std::vector<Vma>& vmas, int pid, int madviseType) {
+    static struct iovec vmasToKernel[MAX_VMAS_PER_COMPACTION];
+
     if (vmas.empty()) {
         return 0;
     }
@@ -281,16 +108,13 @@
         return -errno;
     }
 
-    struct iovec destVmas[MAX_VMAS_PER_BATCH];
-
-    VmaBatch batch;
-    VmaBatchCreator batcher(&vmas, destVmas);
-
     int64_t totalBytesProcessed = 0;
-    while (batcher.createNextBatch(batch)) {
-        uint64_t bytesProcessedInSend;
 
-        do {
+    int64_t vmaOffset = 0;
+    for (int iVma = 0; iVma < vmas.size();) {
+        uint64_t bytesSentToCompact = 0;
+        int iVec = 0;
+        while (iVec < MAX_VMAS_PER_COMPACTION && iVma < vmas.size()) {
             if (CC_UNLIKELY(cancelRunningCompaction.load())) {
                 // There could be a significant delay between when a compaction
                 // is requested and when it is handled during this time our
@@ -300,13 +124,50 @@
                                          StringPrintf("Cancelled compaction for %d", pid).c_str());
                 return ERROR_COMPACTION_CANCELLED;
             }
-            int error = madviseVmasFromBatch(pidfd, batch, madviseType, &bytesProcessedInSend);
-            if (error < 0) {
-                // Returns standard linux errno code
-                return error;
+
+            uint64_t vmaStart = vmas[iVma].start + vmaOffset;
+            uint64_t vmaSize = vmas[iVma].end - vmaStart;
+            if (vmaSize == 0) {
+                goto next_vma;
             }
-            totalBytesProcessed += bytesProcessedInSend;
-        } while (batch.totalBytes > 0);
+            vmasToKernel[iVec].iov_base = (void*)vmaStart;
+            if (vmaSize > MAX_BYTES_PER_COMPACTION - bytesSentToCompact) {
+                // Exceeded the max bytes that could be sent, so clamp
+                // the end to avoid exceeding limit and issue compaction
+                vmaSize = MAX_BYTES_PER_COMPACTION - bytesSentToCompact;
+            }
+
+            vmasToKernel[iVec].iov_len = vmaSize;
+            bytesSentToCompact += vmaSize;
+            ++iVec;
+            if (bytesSentToCompact >= MAX_BYTES_PER_COMPACTION) {
+                // Ran out of bytes within iovec, dispatch compaction.
+                vmaOffset += vmaSize;
+                break;
+            }
+
+        next_vma:
+            // Finished current VMA, and have more bytes remaining
+            vmaOffset = 0;
+            ++iVma;
+        }
+
+        ATRACE_BEGIN(StringPrintf("Compact %d VMAs", iVec).c_str());
+        auto bytesProcessed = process_madvise(pidfd, vmasToKernel, iVec, madviseType, 0);
+        ATRACE_END();
+
+        if (CC_UNLIKELY(bytesProcessed == -1)) {
+            if (errno == EINVAL) {
+                // This error is somewhat common due to an unevictable VMA if this is
+                // the case silently skip the bad VMA and continue compacting the rest.
+                continue;
+            } else {
+                // Forward irrecoverable errors and bail out compaction
+                return -errno;
+            }
+        }
+
+        totalBytesProcessed += bytesProcessed;
     }
 
     return totalBytesProcessed;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 3c5ebe7..32adac7 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -107,6 +107,7 @@
     jmethodID interceptKeyBeforeDispatching;
     jmethodID dispatchUnhandledKey;
     jmethodID checkInjectEventsPermission;
+    jmethodID onPointerDisplayIdChanged;
     jmethodID onPointerDownOutsideFocus;
     jmethodID getVirtualKeyQuietTimeMillis;
     jmethodID getExcludedDeviceNames;
@@ -120,7 +121,6 @@
     jmethodID getLongPressTimeout;
     jmethodID getPointerLayer;
     jmethodID getPointerIcon;
-    jmethodID getPointerDisplayId;
     jmethodID getKeyboardLayoutOverlay;
     jmethodID getDeviceAlias;
     jmethodID getTouchCalibrationForInputDevice;
@@ -277,6 +277,7 @@
     void setFocusedDisplay(int32_t displayId);
     void setInputDispatchMode(bool enabled, bool frozen);
     void setSystemUiLightsOut(bool lightsOut);
+    void setPointerDisplayId(int32_t displayId);
     void setPointerSpeed(int32_t speed);
     void setPointerAcceleration(float acceleration);
     void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
@@ -288,7 +289,6 @@
     void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
     void setCustomPointerIcon(const SpriteIcon& icon);
     void setMotionClassifierEnabled(bool enabled);
-    void notifyPointerDisplayIdChanged();
 
     /* --- InputReaderPolicyInterface implementation --- */
 
@@ -346,6 +346,7 @@
             std::map<int32_t, PointerAnimation>* outAnimationResources, int32_t displayId);
     virtual int32_t getDefaultPointerIconId();
     virtual int32_t getCustomPointerIconId();
+    virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos);
 
 private:
     sp<InputManagerInterface> mInputManager;
@@ -394,7 +395,6 @@
     void updateInactivityTimeoutLocked();
     void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
     void ensureSpriteControllerLocked();
-    int32_t getPointerDisplayId();
     sp<SurfaceControl> getParentSurfaceForPointers(int displayId);
     static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
 
@@ -498,13 +498,9 @@
         }
     }
 
-    // Get the preferred pointer controller displayId.
-    int32_t pointerDisplayId = getPointerDisplayId();
-
     { // acquire lock
         AutoMutex _l(mLock);
         mLocked.viewports = viewports;
-        mLocked.pointerDisplayId = pointerDisplayId;
         std::shared_ptr<PointerController> controller = mLocked.pointerController.lock();
         if (controller != nullptr) {
             controller->onDisplayViewportsUpdated(mLocked.viewports);
@@ -666,15 +662,12 @@
     return controller;
 }
 
-int32_t NativeInputManager::getPointerDisplayId() {
+void NativeInputManager::onPointerDisplayIdChanged(int32_t pointerDisplayId, float xPos,
+                                                   float yPos) {
     JNIEnv* env = jniEnv();
-    jint pointerDisplayId = env->CallIntMethod(mServiceObj,
-            gServiceClassInfo.getPointerDisplayId);
-    if (checkAndClearExceptionFromCallback(env, "getPointerDisplayId")) {
-        pointerDisplayId = ADISPLAY_ID_DEFAULT;
-    }
-
-    return pointerDisplayId;
+    env->CallVoidMethod(mServiceObj, gServiceClassInfo.onPointerDisplayIdChanged, pointerDisplayId,
+                        xPos, yPos);
+    checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged");
 }
 
 sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(int displayId) {
@@ -1032,6 +1025,22 @@
                                                                : InactivityTimeout::NORMAL);
 }
 
+void NativeInputManager::setPointerDisplayId(int32_t displayId) {
+    { // acquire lock
+        AutoMutex _l(mLock);
+
+        if (mLocked.pointerDisplayId == displayId) {
+            return;
+        }
+
+        ALOGI("Setting pointer display id to %d.", displayId);
+        mLocked.pointerDisplayId = displayId;
+    } // release lock
+
+    mInputManager->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+}
+
 void NativeInputManager::setPointerSpeed(int32_t speed) {
     { // acquire lock
         AutoMutex _l(mLock);
@@ -1494,18 +1503,6 @@
     mInputManager->getClassifier().setMotionClassifierEnabled(enabled);
 }
 
-void NativeInputManager::notifyPointerDisplayIdChanged() {
-    int32_t pointerDisplayId = getPointerDisplayId();
-
-    { // acquire lock
-        AutoMutex _l(mLock);
-        mLocked.pointerDisplayId = pointerDisplayId;
-    } // release lock
-
-    mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
-}
-
 // ----------------------------------------------------------------------------
 
 static NativeInputManager* getNativeInputManager(JNIEnv* env, jobject clazz) {
@@ -2199,11 +2196,6 @@
             InputReaderConfiguration::CHANGE_DISPLAY_INFO);
 }
 
-static void nativeNotifyPointerDisplayIdChanged(JNIEnv* env, jobject nativeImplObj) {
-    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    im->notifyPointerDisplayIdChanged();
-}
-
 static void nativeSetDisplayEligibilityForPointerCapture(JNIEnv* env, jobject nativeImplObj,
                                                          jint displayId, jboolean isEligible) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2321,6 +2313,11 @@
     im->getInputManager()->getDispatcher().cancelCurrentTouch();
 }
 
+static void nativeSetPointerDisplayId(JNIEnv* env, jobject nativeImplObj, jint displayId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->setPointerDisplayId(displayId);
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gInputManagerMethods[] = {
@@ -2393,7 +2390,6 @@
         {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay},
         {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged},
         {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation},
-        {"notifyPointerDisplayIdChanged", "()V", (void*)nativeNotifyPointerDisplayIdChanged},
         {"setDisplayEligibilityForPointerCapture", "(IZ)V",
          (void*)nativeSetDisplayEligibilityForPointerCapture},
         {"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled},
@@ -2403,6 +2399,7 @@
         {"disableSensor", "(II)V", (void*)nativeDisableSensor},
         {"flushSensor", "(II)Z", (void*)nativeFlushSensor},
         {"cancelCurrentTouch", "()V", (void*)nativeCancelCurrentTouch},
+        {"setPointerDisplayId", "(I)V", (void*)nativeSetPointerDisplayId},
 };
 
 #define FIND_CLASS(var, className) \
@@ -2498,6 +2495,9 @@
     GET_METHOD_ID(gServiceClassInfo.checkInjectEventsPermission, clazz,
                   "checkInjectEventsPermission", "(II)Z");
 
+    GET_METHOD_ID(gServiceClassInfo.onPointerDisplayIdChanged, clazz, "onPointerDisplayIdChanged",
+                  "(IFF)V");
+
     GET_METHOD_ID(gServiceClassInfo.onPointerDownOutsideFocus, clazz,
             "onPointerDownOutsideFocus", "(Landroid/os/IBinder;)V");
 
@@ -2537,9 +2537,6 @@
     GET_METHOD_ID(gServiceClassInfo.getPointerIcon, clazz,
             "getPointerIcon", "(I)Landroid/view/PointerIcon;");
 
-    GET_METHOD_ID(gServiceClassInfo.getPointerDisplayId, clazz,
-            "getPointerDisplayId", "()I");
-
     GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz,
             "getKeyboardLayoutOverlay",
             "(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;");
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
index 8aa9f60..994a767 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -238,7 +238,7 @@
             }
         }
 
-        // called from Device.close()
+        // called from Device.closeLocked()
         public void removeDeviceConnection(DeviceConnection connection) {
             mDeviceConnections.remove(connection.getToken());
             if (mListeners.size() == 0 && mDeviceConnections.size() == 0) {
@@ -294,12 +294,6 @@
             }
 
             for (DeviceConnection connection : mDeviceConnections.values()) {
-                if (connection.getDevice().getDeviceInfo().getType()
-                        == MidiDeviceInfo.TYPE_USB) {
-                    synchronized (mUsbMidiLock) {
-                        removeUsbMidiDeviceLocked(connection.getDevice().getDeviceInfo());
-                    }
-                }
                 connection.getDevice().removeDeviceConnection(connection);
             }
         }
@@ -541,6 +535,13 @@
                 synchronized (mDeviceConnections) {
                     mDeviceConnections.remove(connection);
 
+                    if (connection.getDevice().getDeviceInfo().getType()
+                            == MidiDeviceInfo.TYPE_USB) {
+                        synchronized (mUsbMidiLock) {
+                            removeUsbMidiDeviceLocked(connection.getDevice().getDeviceInfo());
+                        }
+                    }
+
                     if (mDeviceConnections.size() == 0 && mServiceConnection != null) {
                         mContext.unbindService(mServiceConnection);
                         mServiceConnection = null;
@@ -559,6 +560,12 @@
         public void closeLocked() {
             synchronized (mDeviceConnections) {
                 for (DeviceConnection connection : mDeviceConnections) {
+                    if (connection.getDevice().getDeviceInfo().getType()
+                            == MidiDeviceInfo.TYPE_USB) {
+                        synchronized (mUsbMidiLock) {
+                            removeUsbMidiDeviceLocked(connection.getDevice().getDeviceInfo());
+                        }
+                    }
                     connection.getClient().removeDeviceConnection(connection);
                 }
                 mDeviceConnections.clear();
@@ -1401,6 +1408,8 @@
         String deviceName = extractUsbDeviceName(name);
         String tagName = extractUsbDeviceTag(name);
 
+        Log.i(TAG, "Checking " + deviceName + " " + tagName);
+
         // Only one MIDI 2.0 device can be used at once.
         // Multiple MIDI 1.0 devices can be used at once.
         if (mUsbMidiUniversalDeviceInUse.contains(deviceName)
@@ -1420,6 +1429,8 @@
         String deviceName = extractUsbDeviceName(name);
         String tagName = extractUsbDeviceTag(name);
 
+        Log.i(TAG, "Adding " + deviceName + " " + tagName);
+
         if ((tagName).equals(MIDI_UNIVERSAL_STRING)) {
             mUsbMidiUniversalDeviceInUse.add(deviceName);
         } else if ((tagName).equals(MIDI_LEGACY_STRING)) {
@@ -1437,6 +1448,8 @@
         String deviceName = extractUsbDeviceName(name);
         String tagName = extractUsbDeviceTag(name);
 
+        Log.i(TAG, "Removing " + deviceName + " " + tagName);
+
         if ((tagName).equals(MIDI_UNIVERSAL_STRING)) {
             mUsbMidiUniversalDeviceInUse.remove(deviceName);
         } else if ((tagName).equals(MIDI_LEGACY_STRING)) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 2f68306..8d59dce 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -30,6 +30,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
 import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
 import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
+import static android.app.ActivityManager.PROCESS_STATE_PERSISTENT;
 import static android.app.ActivityManager.PROCESS_STATE_PERSISTENT_UI;
 import static android.app.ActivityManager.PROCESS_STATE_RECEIVER;
 import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
@@ -1591,6 +1592,69 @@
 
     @SuppressWarnings("GuardedBy")
     @Test
+    public void testUpdateOomAdj_DoOne_TreatLikeVisFGS() {
+        final ProcessRecord app1 = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+                MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+        final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+                MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+        final ProcessRecord client1 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+                MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+        final ProcessRecord client2 = spy(makeDefaultProcessRecord(MOCKAPP4_PID, MOCKAPP4_UID,
+                MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
+        client1.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+        client2.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+
+        final ServiceRecord s1 = bindService(app1, client1, null,
+                Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE, mock(IBinder.class));
+        final ServiceRecord s2 = bindService(app2, client2, null,
+                Context.BIND_IMPORTANT, mock(IBinder.class));
+
+        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
+        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
+                SCHED_GROUP_DEFAULT);
+        assertProcStates(app2, PROCESS_STATE_PERSISTENT, PERSISTENT_SERVICE_ADJ,
+                SCHED_GROUP_DEFAULT);
+
+        bindService(app2, client1, s2, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
+                mock(IBinder.class));
+        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        assertProcStates(app2, PROCESS_STATE_PERSISTENT, PERSISTENT_SERVICE_ADJ,
+                SCHED_GROUP_DEFAULT);
+
+        s1.getConnections().clear();
+        s2.getConnections().clear();
+        client1.mState.setMaxAdj(UNKNOWN_ADJ);
+        client2.mState.setMaxAdj(UNKNOWN_ADJ);
+        client1.mServices.setHasForegroundServices(true, 0);
+        client2.mState.setHasOverlayUi(true);
+
+        bindService(app1, client1, s1, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
+                mock(IBinder.class));
+        bindService(app2, client2, s2, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
+                mock(IBinder.class));
+
+        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
+        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+
+        assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                SCHED_GROUP_DEFAULT);
+        assertProcStates(app2, PROCESS_STATE_IMPORTANT_FOREGROUND, PERCEPTIBLE_APP_ADJ,
+                SCHED_GROUP_DEFAULT);
+
+        client2.mState.setHasOverlayUi(false);
+        doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
+        doReturn(client2).when(sService).getTopApp();
+        sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+
+        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        assertProcStates(app2, PROCESS_STATE_BOUND_TOP, VISIBLE_APP_ADJ,
+                SCHED_GROUP_DEFAULT);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
     public void testUpdateOomAdj_UidIdle_StopService() {
         final ProcessRecord app1 = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index b4bb04d..77cbb3a 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -19,22 +19,22 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.IInputManager;
-import android.hardware.input.InputManager;
 import android.hardware.input.InputManagerInternal;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.view.Display;
 import android.view.DisplayInfo;
 
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.server.LocalServices;
 
 import org.junit.Before;
@@ -44,7 +44,8 @@
 import org.mockito.MockitoAnnotations;
 
 @Presubmit
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class InputControllerTest {
 
     @Mock
@@ -56,13 +57,16 @@
     @Mock
     private IInputManager mIInputManagerMock;
 
+    private InputManagerMockHelper mInputManagerMockHelper;
     private InputController mInputController;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        mInputManagerMockHelper = new InputManagerMockHelper(
+                TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
 
-        doNothing().when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
+        doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
         LocalServices.removeServiceForTest(InputManagerInternal.class);
         LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
 
@@ -72,10 +76,10 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
-        InputManager.resetInstance(mIInputManagerMock);
-        doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
-        doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
-        mInputController = new InputController(new Object(), mNativeWrapperMock);
+        // Allow virtual devices to be created on the looper thread for testing.
+        final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
+        mInputController = new InputController(new Object(), mNativeWrapperMock,
+                new Handler(TestableLooper.get(this).getLooper()), threadVerifier);
     }
 
     @Test
@@ -83,6 +87,7 @@
         final IBinder deviceToken = new Binder();
         mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
                 /* displayId= */ 1);
+        verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mInputController.unregisterInputDevice(deviceToken);
@@ -95,10 +100,12 @@
         final IBinder deviceToken = new Binder();
         mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
                 /* displayId= */ 1);
+        verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
         final IBinder deviceToken2 = new Binder();
         mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken2,
                 /* displayId= */ 2);
+        verify(mNativeWrapperMock, times(2)).openUinputMouse(eq("name"), eq(1), eq(1), anyString());
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(2));
         mInputController.unregisterInputDevice(deviceToken);
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
new file mode 100644
index 0000000..aa2d97e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
@@ -0,0 +1,101 @@
+/*
+ * 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.companion.virtual;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+
+import android.hardware.input.IInputDevicesChangedListener;
+import android.hardware.input.IInputManager;
+import android.hardware.input.InputManager;
+import android.os.RemoteException;
+import android.testing.TestableLooper;
+import android.view.InputDevice;
+
+import org.mockito.invocation.InvocationOnMock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+/**
+ * A test utility class used to share the logic for setting up {@link InputManager}'s callback for
+ * when a virtual input device being added.
+ */
+class InputManagerMockHelper {
+    private final TestableLooper mTestableLooper;
+    private final InputController.NativeWrapper mNativeWrapperMock;
+    private final IInputManager mIInputManagerMock;
+    private final List<InputDevice> mDevices = new ArrayList<>();
+    private IInputDevicesChangedListener mDevicesChangedListener;
+
+    InputManagerMockHelper(TestableLooper testableLooper,
+            InputController.NativeWrapper nativeWrapperMock, IInputManager iInputManagerMock)
+            throws Exception {
+        mTestableLooper = testableLooper;
+        mNativeWrapperMock = nativeWrapperMock;
+        mIInputManagerMock = iInputManagerMock;
+
+        doAnswer(this::handleNativeOpenInputDevice).when(mNativeWrapperMock).openUinputMouse(
+                anyString(), anyInt(), anyInt(), anyString());
+        doAnswer(this::handleNativeOpenInputDevice).when(mNativeWrapperMock).openUinputKeyboard(
+                anyString(), anyInt(), anyInt(), anyString());
+        doAnswer(this::handleNativeOpenInputDevice).when(mNativeWrapperMock).openUinputTouchscreen(
+                anyString(), anyInt(), anyInt(), anyString(), anyInt(), anyInt());
+
+        doAnswer(inv -> {
+            mDevicesChangedListener = inv.getArgument(0);
+            return null;
+        }).when(mIInputManagerMock).registerInputDevicesChangedListener(notNull());
+        when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]);
+        doAnswer(inv -> mDevices.get(inv.getArgument(0)))
+                .when(mIInputManagerMock).getInputDevice(anyInt());
+        doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
+        doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
+
+        // Set a new instance of InputManager for testing that uses the IInputManager mock as the
+        // interface to the server.
+        InputManager.resetInstance(mIInputManagerMock);
+    }
+
+    private Void handleNativeOpenInputDevice(InvocationOnMock inv) {
+        Objects.requireNonNull(mDevicesChangedListener,
+                "InputController did not register an InputDevicesChangedListener.");
+        // We only use a subset of the fields of InputDevice in InputController.
+        final InputDevice device = new InputDevice(mDevices.size() /*id*/, 1 /*generation*/, 0,
+                inv.getArgument(0) /*name*/, inv.getArgument(1) /*vendorId*/,
+                inv.getArgument(2) /*productId*/, inv.getArgument(3) /*descriptor*/,
+                true /*isExternal*/, 0 /*sources*/, 0 /*keyboardType*/,
+                null /*keyCharacterMap*/, false /*hasVibrator*/, false /*hasMic*/,
+                false /*hasButtonUnderPad*/, false /*hasSensor*/, false /*hasBattery*/);
+        mDevices.add(device);
+        try {
+            mDevicesChangedListener.onInputDevicesChanged(
+                    mDevices.stream().flatMapToInt(
+                            d -> IntStream.of(d.getId(), d.getGeneration())).toArray());
+        } catch (RemoteException ignored) {
+        }
+        // Process the device added notification.
+        mTestableLooper.processAllMessages();
+        return null;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 808f8c2c..cbb9fd7 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -54,6 +54,7 @@
 import android.content.pm.ApplicationInfo;
 import android.graphics.Point;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.IInputManager;
 import android.hardware.input.InputManagerInternal;
 import android.hardware.input.VirtualKeyEvent;
 import android.hardware.input.VirtualMouseButtonEvent;
@@ -118,6 +119,7 @@
     private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
 
     private Context mContext;
+    private InputManagerMockHelper mInputManagerMockHelper;
     private VirtualDeviceImpl mDeviceImpl;
     private InputController mInputController;
     private AssociationInfo mAssociationInfo;
@@ -146,6 +148,8 @@
     private IAudioConfigChangedCallback mConfigChangedCallback;
     @Mock
     private ApplicationInfo mApplicationInfoMock;
+    @Mock
+    IInputManager mIInputManagerMock;
 
     private ArraySet<ComponentName> getBlockedActivities() {
         ArraySet<ComponentName> blockedActivities = new ArraySet<>();
@@ -170,13 +174,13 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
-        doNothing().when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
+        doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerAcceleration(anyFloat(), anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
         LocalServices.removeServiceForTest(InputManagerInternal.class);
@@ -199,7 +203,13 @@
                 new Handler(TestableLooper.get(this).getLooper()));
         when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
 
-        mInputController = new InputController(new Object(), mNativeWrapperMock);
+        mInputManagerMockHelper = new InputManagerMockHelper(
+                TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
+        // Allow virtual devices to be created on the looper thread for testing.
+        final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
+        mInputController = new InputController(new Object(), mNativeWrapperMock,
+                new Handler(TestableLooper.get(this).getLooper()), threadVerifier);
+
         mAssociationInfo = new AssociationInfo(1, 0, null,
                 MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);
 
diff --git a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
new file mode 100644
index 0000000..cb97c9b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
@@ -0,0 +1,222 @@
+/*
+ * 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.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.display.DisplayViewport
+import android.hardware.input.InputManagerInternal
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.view.Display
+import androidx.test.InstrumentationRegistry
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tests for {@link InputManagerService}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:InputManagerServiceTests
+ */
+@Presubmit
+class InputManagerServiceTests {
+
+    @get:Rule
+    val rule = MockitoJUnit.rule()!!
+
+    @Mock
+    private lateinit var native: NativeInputManagerService
+
+    @Mock
+    private lateinit var wmCallbacks: InputManagerService.WindowManagerCallbacks
+
+    private lateinit var service: InputManagerService
+    private lateinit var localService: InputManagerInternal
+    private lateinit var context: Context
+    private lateinit var testLooper: TestLooper
+
+    @Before
+    fun setup() {
+        context = spy(ContextWrapper(InstrumentationRegistry.getContext()))
+        testLooper = TestLooper()
+        service =
+            InputManagerService(object : InputManagerService.Injector(context, testLooper.looper) {
+                override fun getNativeService(
+                    service: InputManagerService?
+                ): NativeInputManagerService {
+                    return native
+                }
+
+                override fun registerLocalService(service: InputManagerInternal?) {
+                    localService = service!!
+                }
+            })
+        assertTrue("Local service must be registered", this::localService.isInitialized)
+        service.setWindowManagerCallbacks(wmCallbacks)
+    }
+
+    @Test
+    fun testPointerDisplayUpdatesWhenDisplayViewportsChanged() {
+        val displayId = 123
+        `when`(wmCallbacks.pointerDisplayId).thenReturn(displayId)
+        val viewports = listOf<DisplayViewport>()
+        localService.setDisplayViewports(viewports)
+        verify(native).setDisplayViewports(any(Array<DisplayViewport>::class.java))
+        verify(native).setPointerDisplayId(displayId)
+
+        val x = 42f
+        val y = 314f
+        service.onPointerDisplayIdChanged(displayId, x, y)
+        testLooper.dispatchNext()
+        verify(wmCallbacks).notifyPointerDisplayIdChanged(displayId, x, y)
+    }
+
+    @Test
+    fun testSetVirtualMousePointerDisplayId() {
+        // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
+        // until the native callback happens.
+        var countDownLatch = CountDownLatch(1)
+        val overrideDisplayId = 123
+        Thread {
+            assertTrue("Setting virtual pointer display should succeed",
+                localService.setVirtualMousePointerDisplayId(overrideDisplayId))
+            countDownLatch.countDown()
+        }.start()
+        assertFalse("Setting virtual pointer display should block",
+            countDownLatch.await(100, TimeUnit.MILLISECONDS))
+
+        val x = 42f
+        val y = 314f
+        service.onPointerDisplayIdChanged(overrideDisplayId, x, y)
+        testLooper.dispatchNext()
+        verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, x, y)
+        assertTrue("Native callback unblocks calling thread",
+            countDownLatch.await(100, TimeUnit.MILLISECONDS))
+        verify(native).setPointerDisplayId(overrideDisplayId)
+
+        // Ensure that setting the same override again succeeds immediately.
+        assertTrue("Setting the same virtual mouse pointer displayId again should succeed",
+            localService.setVirtualMousePointerDisplayId(overrideDisplayId))
+
+        // Ensure that we did not query WM for the pointerDisplayId when setting the override
+        verify(wmCallbacks, never()).pointerDisplayId
+
+        // Unset the virtual mouse pointer displayId, and ensure that we query WM for the new
+        // pointer displayId and the calling thread is blocked until the native callback happens.
+        countDownLatch = CountDownLatch(1)
+        val pointerDisplayId = 42
+        `when`(wmCallbacks.pointerDisplayId).thenReturn(pointerDisplayId)
+        Thread {
+            assertTrue("Unsetting virtual mouse pointer displayId should succeed",
+                localService.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY))
+            countDownLatch.countDown()
+        }.start()
+        assertFalse("Unsetting virtual mouse pointer displayId should block",
+            countDownLatch.await(100, TimeUnit.MILLISECONDS))
+
+        service.onPointerDisplayIdChanged(pointerDisplayId, x, y)
+        testLooper.dispatchNext()
+        verify(wmCallbacks).notifyPointerDisplayIdChanged(pointerDisplayId, x, y)
+        assertTrue("Native callback unblocks calling thread",
+            countDownLatch.await(100, TimeUnit.MILLISECONDS))
+        verify(native).setPointerDisplayId(pointerDisplayId)
+    }
+
+    @Test
+    fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() {
+        // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
+        // until the native callback happens.
+        val countDownLatch = CountDownLatch(1)
+        val overrideDisplayId = 123
+        Thread {
+            assertFalse("Setting virtual pointer display should be unsuccessful",
+                localService.setVirtualMousePointerDisplayId(overrideDisplayId))
+            countDownLatch.countDown()
+        }.start()
+        assertFalse("Setting virtual pointer display should block",
+            countDownLatch.await(100, TimeUnit.MILLISECONDS))
+
+        val x = 42f
+        val y = 314f
+        // Assume the native callback updates the pointerDisplayId to the incorrect value.
+        service.onPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
+        testLooper.dispatchNext()
+        verify(wmCallbacks).notifyPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
+        assertTrue("Native callback unblocks calling thread",
+            countDownLatch.await(100, TimeUnit.MILLISECONDS))
+        verify(native).setPointerDisplayId(overrideDisplayId)
+    }
+
+    @Test
+    fun testSetVirtualMousePointerDisplayId_competingRequests() {
+        val firstRequestSyncLatch = CountDownLatch(1)
+        doAnswer {
+            firstRequestSyncLatch.countDown()
+        }.`when`(native).setPointerDisplayId(anyInt())
+
+        val firstRequestLatch = CountDownLatch(1)
+        val firstOverride = 123
+        Thread {
+            assertFalse("Setting virtual pointer display from thread 1 should be unsuccessful",
+                localService.setVirtualMousePointerDisplayId(firstOverride))
+            firstRequestLatch.countDown()
+        }.start()
+        assertFalse("Setting virtual pointer display should block",
+            firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
+
+        assertTrue("Wait for first thread's request should succeed",
+            firstRequestSyncLatch.await(100, TimeUnit.MILLISECONDS))
+
+        val secondRequestLatch = CountDownLatch(1)
+        val secondOverride = 42
+        Thread {
+            assertTrue("Setting virtual mouse pointer from thread 2 should be successful",
+                localService.setVirtualMousePointerDisplayId(secondOverride))
+            secondRequestLatch.countDown()
+        }.start()
+        assertFalse("Setting virtual mouse pointer should block",
+            secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
+
+        val x = 42f
+        val y = 314f
+        // Assume the native callback updates directly to the second request.
+        service.onPointerDisplayIdChanged(secondOverride, x, y)
+        testLooper.dispatchNext()
+        verify(wmCallbacks).notifyPointerDisplayIdChanged(secondOverride, x, y)
+        assertTrue("Native callback unblocks first thread",
+            firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
+        assertTrue("Native callback unblocks second thread",
+            secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
+        verify(native, times(2)).setPointerDisplayId(anyInt())
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
new file mode 100644
index 0000000..f330017
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.logcat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.content.ContextWrapper;
+import android.os.ILogd;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.test.TestLooper;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.LocalServices;
+import com.android.server.logcat.LogcatManagerService.Injector;
+import com.android.server.testutils.OffsettableClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Supplier;
+
+/**
+ * Tests for {@link com.android.server.logcat.LogcatManagerService}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:LogcatManagerServiceTest
+ */
+@SuppressWarnings("GuardedBy")
+public class LogcatManagerServiceTest {
+    private static final String APP1_PACKAGE_NAME = "app1";
+    private static final int APP1_UID = 10001;
+    private static final int APP1_GID = 10001;
+    private static final int APP1_PID = 10001;
+    private static final String APP2_PACKAGE_NAME = "app2";
+    private static final int APP2_UID = 10002;
+    private static final int APP2_GID = 10002;
+    private static final int APP2_PID = 10002;
+    private static final int FD1 = 10;
+    private static final int FD2 = 11;
+
+    @Mock
+    private ActivityManagerInternal mActivityManagerInternalMock;
+    @Mock
+    private ILogd mLogdMock;
+
+    private LogcatManagerService mService;
+    private LogcatManagerService.LogcatManagerServiceInternal mLocalService;
+    private ContextWrapper mContextSpy;
+    private OffsettableClock mClock;
+    private TestLooper mTestLooper;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
+        mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+        mClock = new OffsettableClock.Stopped();
+        mTestLooper = new TestLooper(mClock::now);
+
+        when(mActivityManagerInternalMock.getPackageNameByPid(APP1_PID)).thenReturn(
+                APP1_PACKAGE_NAME);
+        when(mActivityManagerInternalMock.getPackageNameByPid(APP2_PID)).thenReturn(
+                APP2_PACKAGE_NAME);
+
+        mService = new LogcatManagerService(mContextSpy, new Injector() {
+            @Override
+            protected Supplier<Long> createClock() {
+                return mClock::now;
+            }
+
+            @Override
+            protected Looper getLooper() {
+                return mTestLooper.getLooper();
+            }
+
+            @Override
+            protected ILogd getLogdService() {
+                return mLogdMock;
+            }
+        });
+        mLocalService = mService.getLocalService();
+        mService.onStart();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+    }
+
+    /**
+     * Creates a mock and registers it to {@link LocalServices}.
+     */
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+
+    @Test
+    public void test_RequestFromBackground_DeclinedWithoutPrompt() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_RECEIVER);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mContextSpy, never()).startActivityAsUser(any(), any());
+    }
+
+    @Test
+    public void test_RequestFromForegroundService_DeclinedWithoutPrompt() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mContextSpy, never()).startActivityAsUser(any(), any());
+    }
+
+    @Test
+    public void test_RequestFromTop_ShowsPrompt() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+    }
+
+    @Test
+    public void test_RequestFromTop_NoInteractionWithPrompt_DeclinesAfterTimeout()
+            throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        advanceTime(LogcatManagerService.PENDING_CONFIRMATION_TIMEOUT_MILLIS);
+
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+    }
+
+    @Test
+    public void test_RequestFromTop_Approved() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+
+        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+    }
+
+    @Test
+    public void test_RequestFromTop_Declined() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+
+        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+    }
+
+    @Test
+    public void test_RequestFromTop_MultipleRequestsApprovedTogether() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
+        mTestLooper.dispatchAll();
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+        verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
+        verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
+
+        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
+    }
+
+    @Test
+    public void test_RequestFromTop_MultipleRequestsDeclinedTogether() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
+        mTestLooper.dispatchAll();
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+        verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
+        verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
+
+        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
+    }
+
+    @Test
+    public void test_RequestFromTop_Approved_DoesNotShowPromptAgain() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
+        mTestLooper.dispatchAll();
+
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
+    }
+
+    @Test
+    public void test_RequestFromTop_Declined_DoesNotShowPromptAgain() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
+        mTestLooper.dispatchAll();
+
+        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
+        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
+    }
+
+    @Test
+    public void test_RequestFromTop_Approved_ShowsPromptForDifferentClient() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        when(mActivityManagerInternalMock.getUidProcessState(APP2_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        mService.getBinderService().startThread(APP2_UID, APP2_GID, APP2_PID, FD2);
+        mTestLooper.dispatchAll();
+
+        verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+        verify(mLogdMock, never()).decline(APP2_UID, APP2_GID, APP2_PID, FD2);
+        verify(mLogdMock, never()).approve(APP2_UID, APP2_GID, APP2_PID, FD2);
+    }
+
+    @Test
+    public void test_RequestFromTop_Approved_ShowPromptAgainAfterTimeout() throws Exception {
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_TOP);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mTestLooper.dispatchAll();
+
+        advanceTime(LogcatManagerService.STATUS_EXPIRATION_TIMEOUT_MILLIS);
+
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
+    }
+
+    private void advanceTime(long timeMs) {
+        mClock.fastForward(timeMs);
+        mTestLooper.dispatchAll();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 4094377..fdf9354 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -1975,7 +1975,7 @@
         if (si == null) {
             return null;
         }
-        mService.waitForBitmapSaves();
+        mService.waitForBitmapSavesForTest();
         return new File(si.getBitmapPath()).getName();
     }
 
@@ -1984,7 +1984,7 @@
         if (si == null) {
             return null;
         }
-        mService.waitForBitmapSaves();
+        mService.waitForBitmapSavesForTest();
         return new File(si.getBitmapPath()).getAbsolutePath();
     }
 
@@ -2139,7 +2139,7 @@
     }
 
     protected boolean bitmapDirectoryExists(String packageName, int userId) {
-        mService.waitForBitmapSaves();
+        mService.waitForBitmapSavesForTest();
         final File path = new File(mService.getUserBitmapFilePath(userId), packageName);
         return path.isDirectory();
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 411b521..867890f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -1040,7 +1040,7 @@
 
         dumpsysOnLogcat();
 
-        mService.waitForBitmapSaves();
+        mService.waitForBitmapSavesForTest();
         // Check files and directories.
         // Package 3 has no bitmaps, so we don't create a directory.
         assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
@@ -1096,7 +1096,7 @@
         makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "3").createNewFile();
         makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "4").createNewFile();
 
-        mService.waitForBitmapSaves();
+        mService.waitForBitmapSavesForTest();
         assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3,
                 "a.b.c", "d.e.f");
 
@@ -1111,7 +1111,7 @@
         // The below check is the same as above, except this time USER_0 use the CALLING_PACKAGE_3
         // directory.
 
-        mService.waitForBitmapSaves();
+        mService.waitForBitmapSavesForTest();
         assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3);
         assertBitmapDirectories(USER_10, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
 
@@ -1390,7 +1390,7 @@
                             .setIcon(Icon.createWithContentUri("test_uri"))
                             .build()
             )));
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconUri());
@@ -1402,13 +1402,13 @@
                             .setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
                             .build()
             )));
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconResource());
                         assertEquals(R.drawable.black_32x32, si.getIconResourceId());
                     });
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
 
             mInjectedCurrentTimeMillis += INTERVAL; // reset throttling
 
@@ -1419,7 +1419,7 @@
                                     getTestContext().getResources(), R.drawable.black_64x64)))
                             .build()
             )));
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconFile());
@@ -1437,7 +1437,7 @@
                                     getTestContext().getResources(), R.drawable.black_64x64)))
                             .build()
             )));
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconFile());
@@ -1451,7 +1451,7 @@
                             .setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
                             .build()
             )));
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconResource());
@@ -1463,7 +1463,7 @@
                             .setIcon(Icon.createWithContentUri("test_uri"))
                             .build()
             )));
-            mService.waitForBitmapSaves();
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconUri());
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index f2495e1..9ff7d69 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -71,7 +71,6 @@
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.PowerSaveState;
-import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.test.TestLooper;
 import android.provider.Settings;
@@ -144,6 +143,7 @@
     @Mock private AmbientDisplayConfiguration mAmbientDisplayConfigurationMock;
     @Mock private SystemPropertiesWrapper mSystemPropertiesMock;
     @Mock private AppOpsManager mAppOpsManagerMock;
+    @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock;
 
     @Mock
     private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@@ -298,8 +298,7 @@
             @Override
             LowPowerStandbyController createLowPowerStandbyController(Context context,
                     Looper looper) {
-                return new LowPowerStandbyController(context, mTestLooper.getLooper(),
-                        SystemClock::elapsedRealtime);
+                return mLowPowerStandbyControllerMock;
             }
 
             @Override
@@ -316,7 +315,6 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.removeServiceForTest(BatteryManagerInternal.class);
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
-        LocalServices.removeServiceForTest(LowPowerStandbyControllerInternal.class);
         FakeSettingsProvider.clearSettingsProvider();
     }
 
@@ -1888,6 +1886,18 @@
         assertThat(wakeLock.mDisabled).isFalse();
     }
 
+    @Test
+    public void testSetLowPowerStandbyActiveDuringMaintenance_redirectsCallToNativeWrapper() {
+        createService();
+        startSystem();
+
+        mService.getBinderServiceInstance().setLowPowerStandbyActiveDuringMaintenance(true);
+        verify(mLowPowerStandbyControllerMock).setActiveDuringMaintenance(true);
+
+        mService.getBinderServiceInstance().setLowPowerStandbyActiveDuringMaintenance(false);
+        verify(mLowPowerStandbyControllerMock).setActiveDuringMaintenance(false);
+    }
+
     private WakeLock acquireWakeLock(String tag, int flags) {
         IBinder token = new Binder();
         String packageName = "pkg.name";
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 348e015..c0cd7a7 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -198,6 +198,7 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.InstanceIdSequenceFake;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
@@ -303,6 +304,8 @@
     ActivityManagerInternal mAmi;
     @Mock
     private Looper mMainLooper;
+    @Mock
+    private NotificationManager mMockNm;
 
     @Mock
     IIntentSender pi1;
@@ -405,6 +408,7 @@
         LocalServices.removeServiceForTest(PermissionPolicyInternal.class);
         LocalServices.addService(PermissionPolicyInternal.class, mPermissionPolicyInternal);
         mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager);
+        mContext.addMockSystemService(NotificationManager.class, mMockNm);
 
         doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
 
@@ -7516,46 +7520,53 @@
     }
 
     @Test
-    public void testOnBubbleNotificationSuppressionChanged() throws Exception {
+    public void testOnBubbleMetadataFlagChanged() throws Exception {
         setUpPrefsForBubbles(PKG, mUid,
                 true /* global */,
                 BUBBLE_PREFERENCE_ALL /* app */,
                 true /* channel */);
 
-        // Bubble notification
+        // Post a bubble notification
         NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag");
-
+        // Set this so that the bubble can be suppressed
+        nr.getNotification().getBubbleMetadata().setFlags(
+                Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE);
         mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
                 nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
         waitForIdle();
 
-        // NOT suppressed
+        // Check the flags
         Notification n =  mBinderService.getActiveNotifications(PKG)[0].getNotification();
         assertFalse(n.getBubbleMetadata().isNotificationSuppressed());
+        assertFalse(n.getBubbleMetadata().getAutoExpandBubble());
+        assertFalse(n.getBubbleMetadata().isBubbleSuppressed());
+        assertTrue(n.getBubbleMetadata().isBubbleSuppressable());
 
         // Reset as this is called when the notif is first sent
         reset(mListeners);
 
-        // Test: update suppression to true
-        mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), true,
-                false);
+        // Test: change the flags
+        int flags = Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE;
+        flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
+        flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+        flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
+        mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), flags);
         waitForIdle();
 
         // Check
         n =  mBinderService.getActiveNotifications(PKG)[0].getNotification();
-        assertTrue(n.getBubbleMetadata().isNotificationSuppressed());
+        assertEquals(flags, n.getBubbleMetadata().getFlags());
 
         // Reset to check again
         reset(mListeners);
 
-        // Test: update suppression to false
-        mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), false,
-                false);
+        // Test: clear flags
+        mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), 0);
         waitForIdle();
 
         // Check
         n = mBinderService.getActiveNotifications(PKG)[0].getNotification();
-        assertFalse(n.getBubbleMetadata().isNotificationSuppressed());
+        assertEquals(0, n.getBubbleMetadata().getFlags());
     }
 
     @Test
@@ -9294,4 +9305,77 @@
         // the notifyPostedLocked function is called twice.
         verify(mListeners, times(2)).notifyPostedLocked(any(), any());
     }
+
+    @Test
+    public void testMaybeShowReviewPermissionsNotification_unknown() {
+        // Set up various possible states of the settings int and confirm whether or not the
+        // notification is shown as expected
+
+        // Initial state: default/unknown setting, make sure nothing happens
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
+        mService.maybeShowInitialReviewPermissionsNotification();
+        verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
+    }
+
+    @Test
+    public void testMaybeShowReviewPermissionsNotification_shouldShow() {
+        // If state is SHOULD_SHOW, it ... should show
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+        mService.maybeShowInitialReviewPermissionsNotification();
+        verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
+                eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
+                any(Notification.class));
+    }
+
+    @Test
+    public void testMaybeShowReviewPermissionsNotification_alreadyShown() {
+        // If state is either USER_INTERACTED or DISMISSED, we should not show this on boot
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+        mService.maybeShowInitialReviewPermissionsNotification();
+
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
+        mService.maybeShowInitialReviewPermissionsNotification();
+
+        verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
+    }
+
+    @Test
+    public void testMaybeShowReviewPermissionsNotification_reshown() {
+        // If we have re-shown the notification and the user did not subsequently interacted with
+        // it, then make sure we show when trying on boot
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+        mService.maybeShowInitialReviewPermissionsNotification();
+        verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
+                eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
+                any(Notification.class));
+    }
+
+    @Test
+    public void testRescheduledReviewPermissionsNotification() {
+        // when rescheduled, the notification goes through the NotificationManagerInternal service
+        // this call doesn't need to know anything about previously scheduled state -- if called,
+        // it should send the notification & write the appropriate int to Settings
+        mInternalService.sendReviewPermissionsNotification();
+
+        // Notification should be sent
+        verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
+                eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
+                any(Notification.class));
+
+        // write STATE_RESHOWN to settings
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 63d7453..6d08959 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -289,6 +289,11 @@
                 .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
                 .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
                 .build();
+
+        // make sure that the settings for review notification permissions are unset to begin with
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
     }
 
     private ByteArrayOutputStream writeXmlAndPurge(
@@ -656,6 +661,13 @@
         verify(mPermissionHelper).setNotificationPermission(nMr1Expected);
         verify(mPermissionHelper).setNotificationPermission(oExpected);
         verify(mPermissionHelper).setNotificationPermission(pExpected);
+
+        // verify that we also write a state for review_permissions_notification to eventually
+        // show a notification
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
     }
 
     @Test
@@ -738,7 +750,7 @@
     }
 
     @Test
-    public void testReadXml_newXml_noMigration() throws Exception {
+    public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
         when(mPermissionHelper.isMigrationEnabled()).thenReturn(true);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory);
@@ -786,6 +798,70 @@
         compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false));
 
         verify(mPermissionHelper, never()).setNotificationPermission(any());
+
+        // verify that we do, however, write a state for review_permissions_notification to
+        // eventually show a notification, since this XML version is older than the notification
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+
+    @Test
+    public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
+        when(mPermissionHelper.isMigrationEnabled()).thenReturn(true);
+        mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory);
+
+        String xml = "<ranking version=\"4\">\n"
+                + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
+                + "<channel id=\"idn\" name=\"name\" importance=\"2\"/>\n"
+                + "<channel id=\"miscellaneous\" name=\"Uncategorized\" />\n"
+                + "</package>\n"
+                + "<package name=\"" + PKG_O + "\" >\n"
+                + "<channel id=\"ido\" name=\"name2\" importance=\"2\" show_badge=\"true\"/>\n"
+                + "</package>\n"
+                + "<package name=\"" + PKG_P + "\" >\n"
+                + "<channel id=\"idp\" name=\"name3\" importance=\"4\" locked=\"2\" />\n"
+                + "</package>\n"
+                + "</ranking>\n";
+        NotificationChannel idn = new NotificationChannel("idn", "name", IMPORTANCE_LOW);
+        idn.setSound(null, new AudioAttributes.Builder()
+                .setUsage(USAGE_NOTIFICATION)
+                .setContentType(CONTENT_TYPE_SONIFICATION)
+                .setFlags(0)
+                .build());
+        idn.setShowBadge(false);
+        NotificationChannel ido = new NotificationChannel("ido", "name2", IMPORTANCE_LOW);
+        ido.setShowBadge(true);
+        ido.setSound(null, new AudioAttributes.Builder()
+                .setUsage(USAGE_NOTIFICATION)
+                .setContentType(CONTENT_TYPE_SONIFICATION)
+                .setFlags(0)
+                .build());
+        NotificationChannel idp = new NotificationChannel("idp", "name3", IMPORTANCE_HIGH);
+        idp.lockFields(2);
+        idp.setSound(null, new AudioAttributes.Builder()
+                .setUsage(USAGE_NOTIFICATION)
+                .setContentType(CONTENT_TYPE_SONIFICATION)
+                .setFlags(0)
+                .build());
+
+        loadByteArrayXml(xml.getBytes(), true, USER_SYSTEM);
+
+        assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1));
+
+        assertEquals(idn, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, idn.getId(), false));
+        compareChannels(ido, mHelper.getNotificationChannel(PKG_O, UID_O, ido.getId(), false));
+        compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false));
+
+        verify(mPermissionHelper, never()).setNotificationPermission(any());
+
+        // this XML is new enough, we should not be attempting to show a notification or anything
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
     }
 
     @Test
@@ -903,7 +979,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(
                 PKG_N_MR1, UID_N_MR1, false, USER_SYSTEM);
-        String expected = "<ranking version=\"3\">\n"
+        String expected = "<ranking version=\"4\">\n"
                 + "<package name=\"com.example.o\" show_badge=\"true\" "
                 + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
                 + "sent_valid_msg=\"false\" user_demote_msg_app=\"false\" uid=\"1111\">\n"
@@ -984,7 +1060,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(
                 PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
-        String expected = "<ranking version=\"3\">\n"
+        String expected = "<ranking version=\"4\">\n"
                 // Importance 0 because off in permissionhelper
                 + "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" "
                 + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
@@ -1067,7 +1143,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(
                 PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
-        String expected = "<ranking version=\"3\">\n"
+        String expected = "<ranking version=\"4\">\n"
                 // Importance 0 because off in permissionhelper
                 + "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" "
                 + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
@@ -1121,7 +1197,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(
                 PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
-        String expected = "<ranking version=\"3\">\n"
+        String expected = "<ranking version=\"4\">\n"
                 // Packages that exist solely in permissionhelper
                 + "<package name=\"" + PKG_P + "\" importance=\"3\" />\n"
                 + "<package name=\"" + PKG_O + "\" importance=\"0\" />\n"
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java
new file mode 100644
index 0000000..5a4ce5da
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.notification;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.rule.ServiceTestRule;
+
+import com.android.server.LocalServices;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+@RunWith(AndroidTestingRunner.class)
+public class ReviewNotificationPermissionsJobServiceTest extends UiServiceTestCase {
+    private ReviewNotificationPermissionsJobService mJobService;
+    private JobParameters mJobParams = new JobParameters(null,
+            ReviewNotificationPermissionsJobService.JOB_ID, null, null, null,
+            0, false, false, null, null, null);
+
+    @Captor
+    ArgumentCaptor<JobInfo> mJobInfoCaptor;
+
+    @Mock
+    private JobScheduler mMockJobScheduler;
+
+    @Mock
+    private NotificationManagerInternal mMockNotificationManagerInternal;
+
+    @Rule
+    public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+    @Before
+    public void setUp() throws Exception {
+        mJobService = new ReviewNotificationPermissionsJobService();
+        mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler);
+
+        // add NotificationManagerInternal to LocalServices
+        LocalServices.removeServiceForTest(NotificationManagerInternal.class);
+        LocalServices.addService(NotificationManagerInternal.class,
+                mMockNotificationManagerInternal);
+    }
+
+    @Test
+    public void testScheduleJob() {
+        // if asked, the job doesn't currently exist yet
+        when(mMockJobScheduler.getPendingJob(anyInt())).thenReturn(null);
+
+        final int rescheduleTimeMillis = 350;  // arbitrary number
+
+        // attempt to schedule the job
+        ReviewNotificationPermissionsJobService.scheduleJob(mContext, rescheduleTimeMillis);
+        verify(mMockJobScheduler, times(1)).schedule(mJobInfoCaptor.capture());
+
+        // verify various properties of the job that is passed in to the job scheduler
+        JobInfo jobInfo = mJobInfoCaptor.getValue();
+        assertEquals(ReviewNotificationPermissionsJobService.JOB_ID, jobInfo.getId());
+        assertEquals(rescheduleTimeMillis, jobInfo.getMinLatencyMillis());
+        assertTrue(jobInfo.isPersisted());  // should continue after reboot
+        assertFalse(jobInfo.isPeriodic());  // one time
+    }
+
+    @Test
+    public void testOnStartJob() {
+        // the job need not be persisted after it does its work, so it'll return
+        // false
+        assertFalse(mJobService.onStartJob(mJobParams));
+
+        // verify that starting the job causes the notification to be sent
+        verify(mMockNotificationManagerInternal).sendReviewPermissionsNotification();
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java
new file mode 100644
index 0000000..12281a7
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.notification;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class ReviewNotificationPermissionsReceiverTest extends UiServiceTestCase {
+
+    // Simple mock class that just overrides the reschedule and cancel behavior so that it's easy
+    // to tell whether the receiver has sent requests to either reschedule or cancel the
+    // notification (or both).
+    private class MockReviewNotificationPermissionsReceiver
+            extends ReviewNotificationPermissionsReceiver {
+        boolean mCanceled = false;
+        boolean mRescheduled = false;
+
+        @Override
+        protected void cancelNotification(Context context) {
+            mCanceled = true;
+        }
+
+        @Override
+        protected void rescheduleNotification(Context context) {
+            mRescheduled = true;
+        }
+    }
+
+    private MockReviewNotificationPermissionsReceiver mReceiver;
+    private Intent mIntent;
+
+    @Before
+    public void setUp() {
+        mReceiver = new MockReviewNotificationPermissionsReceiver();
+        mIntent = new Intent();  // actions will be set in test cases
+    }
+
+    @Test
+    public void testReceive_remindMeLater_firstTime() {
+        // Test what happens when we receive a "remind me later" intent coming from
+        // a previously-not-interacted notification
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+
+        // set up Intent action
+        mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
+
+        // Upon receipt of the intent, the following things should happen:
+        //   - notification rescheduled
+        //   - notification explicitly canceled
+        //   - settings state updated to indicate user has interacted
+        mReceiver.onReceive(mContext, mIntent);
+        assertTrue(mReceiver.mRescheduled);
+        assertTrue(mReceiver.mCanceled);
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+
+    @Test
+    public void testReceive_remindMeLater_laterTimes() {
+        // Test what happens when we receive a "remind me later" intent coming from
+        // a previously-interacted notification that has been rescheduled
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+
+        // set up Intent action
+        mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
+
+        // Upon receipt of the intent, the following things should still happen
+        // regardless of the fact that the user has interacted before:
+        //   - notification rescheduled
+        //   - notification explicitly canceled
+        //   - settings state still indicate user has interacted
+        mReceiver.onReceive(mContext, mIntent);
+        assertTrue(mReceiver.mRescheduled);
+        assertTrue(mReceiver.mCanceled);
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+
+    @Test
+    public void testReceive_dismiss() {
+        // Test that dismissing the notification does *not* reschedule the notification,
+        // does cancel it, and writes that it has been dismissed to settings
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+
+        // set up Intent action
+        mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS);
+
+        // send intent, watch what happens
+        mReceiver.onReceive(mContext, mIntent);
+        assertFalse(mReceiver.mRescheduled);
+        assertTrue(mReceiver.mCanceled);
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+
+    @Test
+    public void testReceive_notificationCanceled_firstSwipe() {
+        // Test the basic swipe away case: the first time the user swipes the notification
+        // away, it will not have been interacted with yet, so make sure it's rescheduled
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+
+        // set up Intent action, would be called from notification's delete intent
+        mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+
+        // send intent, make sure it gets:
+        //   - rescheduled
+        //   - not explicitly canceled, the notification was already canceled
+        //   - noted that it's been interacted with
+        mReceiver.onReceive(mContext, mIntent);
+        assertTrue(mReceiver.mRescheduled);
+        assertFalse(mReceiver.mCanceled);
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+
+    @Test
+    public void testReceive_notificationCanceled_secondSwipe() {
+        // Test the swipe away case for a rescheduled notification: in this case
+        // it should not be rescheduled anymore
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+
+        // set up Intent action, would be called from notification's delete intent
+        mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+
+        // send intent, make sure it gets:
+        //   - not rescheduled on the second+ swipe
+        //   - not explicitly canceled, the notification was already canceled
+        //   - mark as user interacted
+        mReceiver.onReceive(mContext, mIntent);
+        assertFalse(mReceiver.mRescheduled);
+        assertFalse(mReceiver.mCanceled);
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+
+    @Test
+    public void testReceive_notificationCanceled_fromDismiss() {
+        // Test that if the notification delete intent is called due to us canceling
+        // the notification from the receiver, we don't do anything extra
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
+
+        // set up Intent action, would be called from notification's delete intent
+        mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+
+        // nothing should happen, nothing at all
+        mReceiver.onReceive(mContext, mIntent);
+        assertFalse(mReceiver.mRescheduled);
+        assertFalse(mReceiver.mCanceled);
+        assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED,
+                Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                        NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 908de34..f59ec42 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.Activity.RESULT_CANCELED;
 import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.ActivityManager.START_ABORTED;
 import static android.app.ActivityManager.START_CANCELED;
@@ -1297,6 +1298,22 @@
         assertEquals(targetRecord.getLaunchIntoPipHostActivity(), sourceRecord);
     }
 
+    @Test
+    public void testResultCanceledWhenNotAllowedStartingActivity() {
+        final ActivityStarter starter = prepareStarter(0, false);
+        final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build();
+        final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).build();
+        targetRecord.resultTo = sourceRecord;
+
+        // Abort the activity start and ensure the sourceRecord gets the result (RESULT_CANCELED).
+        spyOn(starter);
+        doReturn(START_ABORTED).when(starter).isAllowedToStart(any(), anyBoolean(), any());
+        startActivityInner(starter, targetRecord, sourceRecord, null /* options */,
+                null /* inTask */, null /* inTaskFragment */);
+        verify(sourceRecord).sendResult(anyInt(), any(), anyInt(), eq(RESULT_CANCELED), any(),
+                any());
+    }
+
     private static void startActivityInner(ActivityStarter starter, ActivityRecord target,
             ActivityRecord source, ActivityOptions options, Task inTask,
             TaskFragment inTaskFragment) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index eb6395b..3592158 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -39,8 +39,10 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
@@ -48,7 +50,9 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 
 import android.graphics.Rect;
 import android.os.Binder;
@@ -376,7 +380,7 @@
         doReturn(false).when(dc).onDescendantOrientationChanged(any());
         final WindowState exitingAppWindow = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
                 dc, "exiting app");
-        final ActivityRecord exitingActivity= exitingAppWindow.mActivityRecord;
+        final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord;
         // Wait until everything in animation handler get executed to prevent the exiting window
         // from being removed during WindowSurfacePlacer Traversal.
         waitUntilHandlersIdle();
@@ -405,6 +409,41 @@
     }
 
     @Test
+    public void testDelayWhileRecents() {
+        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
+        doReturn(false).when(dc).onDescendantOrientationChanged(any());
+        final Task task = createTask(dc);
+
+        // Simulate activity1 launches activity2.
+        final ActivityRecord activity1 = createActivityRecord(task);
+        activity1.setVisible(true);
+        activity1.mVisibleRequested = false;
+        activity1.allDrawn = true;
+        final ActivityRecord activity2 = createActivityRecord(task);
+        activity2.setVisible(false);
+        activity2.mVisibleRequested = true;
+        activity2.allDrawn = true;
+
+        dc.mClosingApps.add(activity1);
+        dc.mOpeningApps.add(activity2);
+        dc.prepareAppTransition(TRANSIT_OPEN);
+        assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_OPEN));
+
+        // Wait until everything in animation handler get executed to prevent the exiting window
+        // from being removed during WindowSurfacePlacer Traversal.
+        waitUntilHandlersIdle();
+
+        // Start recents
+        doReturn(true).when(task)
+                .isSelfAnimating(anyInt(), eq(ANIMATION_TYPE_RECENTS));
+
+        dc.mAppTransitionController.handleAppTransitionReady();
+
+        verify(activity1, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean());
+        verify(activity2, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean());
+    }
+
+    @Test
     public void testGetAnimationStyleResId() {
         // Verify getAnimationStyleResId will return as LayoutParams.windowAnimations when without
         // specifying window type.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 263c936..c5f785e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1184,6 +1184,31 @@
     }
 
     @Test
+    public void testComputeImeParent_remoteControlTarget() throws Exception {
+        final DisplayContent dc = mDisplayContent;
+        WindowState app1 = createWindow(null, TYPE_BASE_APPLICATION, "app1");
+        WindowState app2 = createWindow(null, TYPE_BASE_APPLICATION, "app2");
+
+        dc.setImeLayeringTarget(app1);
+        dc.setImeInputTarget(app2);
+        dc.setRemoteInsetsController(createDisplayWindowInsetsController());
+        dc.getImeTarget(IME_TARGET_LAYERING).getWindow().setWindowingMode(
+                WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
+        dc.getImeInputTarget().getWindowState().setWindowingMode(
+                WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
+
+        // Expect ImeParent is null since ImeLayeringTarget and ImeInputTarget are different.
+        assertNull(dc.computeImeParent());
+
+        // ImeLayeringTarget and ImeInputTarget are updated to the same.
+        dc.setImeInputTarget(app1);
+        assertEquals(dc.getImeTarget(IME_TARGET_LAYERING), dc.getImeInputTarget());
+
+        // The ImeParent should be the display.
+        assertEquals(dc.getImeContainer().getParent().getSurfaceControl(), dc.computeImeParent());
+    }
+
+    @Test
     public void testInputMethodInputTarget_isClearedWhenWindowStateIsRemoved() throws Exception {
         final DisplayContent dc = createNewDisplay();
 
diff --git a/services/voiceinteraction/TEST_MAPPING b/services/voiceinteraction/TEST_MAPPING
index bc8c639..22a6445 100644
--- a/services/voiceinteraction/TEST_MAPPING
+++ b/services/voiceinteraction/TEST_MAPPING
@@ -5,19 +5,6 @@
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        // TODO(b/225076204): Remove the following four test cases after fixing the test fail.
-        {
-          "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_createDetectorTwiceQuickly_triggerSuccess"
-        },
-        {
-          "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_onDetectFromExternalSource_success"
-        },
-        {
-          "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_createDetectorTwiceQuickly_triggerSuccess"
-        },
-        {
-          "exclude-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest#testHotwordDetectionService_onDetectFromMic_success"
         }
       ]
     },
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index f31cdcb..43fcc8f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -323,6 +323,16 @@
             new RoleObserver(mContext.getMainExecutor());
         }
 
+        void handleUserStop(String packageName, int userHandle) {
+            synchronized (VoiceInteractionManagerServiceStub.this) {
+                ComponentName curInteractor = getCurInteractor(userHandle);
+                if (curInteractor != null && packageName.equals(curInteractor.getPackageName())) {
+                    Slog.d(TAG, "switchImplementation for user stop.");
+                    switchImplementationIfNeededLocked(true);
+                }
+            }
+        }
+
         @Override
         public @NonNull IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator(
                 @NonNull Identity originatorIdentity, IBinder client) {
@@ -2071,6 +2081,7 @@
         }
 
         PackageMonitor mPackageMonitor = new PackageMonitor() {
+
             @Override
             public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
                 if (DEBUG) Slog.d(TAG, "onHandleForceStop uid=" + uid + " doit=" + doit);
@@ -2103,11 +2114,17 @@
                         }
 
                         setCurInteractor(null, userHandle);
+                        // TODO: should not reset null here. But even remove this line, the
+                        // initForUser() still reset it because the interactor will be null. Keep
+                        // it now but we should still need to fix it.
                         setCurRecognizer(null, userHandle);
                         resetCurAssistant(userHandle);
                         initForUser(userHandle);
                         switchImplementationIfNeededLocked(true);
 
+                        // When resetting the interactor, the recognizer and the assistant settings
+                        // value, we also need to reset the assistant role to keep the values
+                        // consistent. Clear the assistant role will reset to the default value.
                         Context context = getContext();
                         context.getSystemService(RoleManager.class).clearRoleHoldersAsUser(
                                 RoleManager.ROLE_ASSISTANT, 0, UserHandle.of(userHandle),
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index edf1002..0558648 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -29,6 +29,7 @@
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.AppGlobals;
+import android.app.ApplicationExitInfo;
 import android.app.IActivityManager;
 import android.app.IActivityTaskManager;
 import android.content.BroadcastReceiver;
@@ -38,6 +39,7 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
 import android.content.pm.ServiceInfo;
 import android.hardware.soundtrigger.IRecognitionStatusCallback;
 import android.hardware.soundtrigger.SoundTrigger;
@@ -149,6 +151,32 @@
                 resetHotwordDetectionConnectionLocked();
             }
         }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            Slog.d(TAG, "onBindingDied to " + name);
+            String packageName = name.getPackageName();
+            ParceledListSlice<ApplicationExitInfo> plistSlice = null;
+            try {
+                plistSlice = mAm.getHistoricalProcessExitReasons(packageName, 0, 1, mUser);
+            } catch (RemoteException e) {
+                // do nothing. The local binder so it can not throw it.
+            }
+            if (plistSlice == null) {
+                return;
+            }
+            List<ApplicationExitInfo> list = plistSlice.getList();
+            if (list.isEmpty()) {
+                return;
+            }
+            // TODO(b/229956310): Refactor the logic of PackageMonitor and onBindingDied
+            ApplicationExitInfo info = list.get(0);
+            if (info.getReason() == ApplicationExitInfo.REASON_USER_REQUESTED
+                    && info.getSubReason() == ApplicationExitInfo.SUBREASON_STOP_APP) {
+                // only handle user stopped the application from the task manager
+                mServiceStub.handleUserStop(packageName, mUser);
+            }
+        }
     };
 
     VoiceInteractionManagerServiceImpl(Context context, Handler handler,
diff --git a/telephony/java/android/telephony/ims/ProvisioningManager.java b/telephony/java/android/telephony/ims/ProvisioningManager.java
index 2833489..f65b7c2 100644
--- a/telephony/java/android/telephony/ims/ProvisioningManager.java
+++ b/telephony/java/android/telephony/ims/ProvisioningManager.java
@@ -1500,8 +1500,8 @@
      * Get the provisioning status for the IMS RCS capability specified.
      *
      * If provisioning is not required for the queried
-     * {@link ImsRcsManager.RcsImsCapabilityFlag} this method will always return
-     * {@code true}.
+     * {@link ImsRcsManager.RcsImsCapabilityFlag} or if the device does not support IMS
+     * this method will always return {@code true}.
      *
      * @see CarrierConfigManager.Ims#KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL
      * @return true if the device is provisioned for the capability or does not require
@@ -1530,8 +1530,8 @@
      * Get the provisioning status for the IMS RCS capability specified.
      *
      * If provisioning is not required for the queried
-     * {@link ImsRcsManager.RcsImsCapabilityFlag} this method
-     * will always return {@code true}.
+     * {@link ImsRcsManager.RcsImsCapabilityFlag} or if the device does not support IMS
+     * this method will always return {@code true}.
      *
      * <p> Requires Permission:
      * <ul>
@@ -1640,7 +1640,8 @@
      * </ul>
      *
      * @return true if provisioning is required for the MMTEL capability and IMS
-     * registration technology specified, false if it is not required.
+     * registration technology specified, false if it is not required or if the device does not
+     * support IMS.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public boolean isProvisioningRequiredForCapability(
@@ -1667,7 +1668,8 @@
      * </ul>
      *
      * @return true if provisioning is required for the RCS capability and IMS
-     * registration technology specified, false if it is not required.
+     * registration technology specified, false if it is not required or if the device does not
+     * support IMS.
      */
     @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
     public boolean isRcsProvisioningRequiredForCapability(